From d33d3663ecb166461d9cb5a78a29b44ee7a8fbb7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 18 Feb 2016 13:03:40 +0100 Subject: combine projects --- mitmproxy/README.rst | 73 - mitmproxy/__init__.py | 0 mitmproxy/cmdline.py | 789 + mitmproxy/console/__init__.py | 744 + mitmproxy/console/common.py | 444 + mitmproxy/console/flowdetailview.py | 153 + mitmproxy/console/flowlist.py | 397 + mitmproxy/console/flowview.py | 714 + mitmproxy/console/grideditor.py | 716 + mitmproxy/console/help.py | 117 + mitmproxy/console/options.py | 271 + mitmproxy/console/palettepicker.py | 82 + mitmproxy/console/palettes.py | 326 + mitmproxy/console/pathedit.py | 71 + mitmproxy/console/searchable.py | 93 + mitmproxy/console/select.py | 120 + mitmproxy/console/signals.py | 43 + mitmproxy/console/statusbar.py | 258 + mitmproxy/console/tabs.py | 70 + mitmproxy/console/window.py | 90 + mitmproxy/contentviews.py | 622 + mitmproxy/contrib/README | 14 + mitmproxy/contrib/__init__.py | 0 mitmproxy/contrib/jsbeautifier/__init__.py | 1153 + .../jsbeautifier/unpackers/README.specs.mkd | 25 + .../contrib/jsbeautifier/unpackers/__init__.py | 66 + .../contrib/jsbeautifier/unpackers/evalbased.py | 39 + .../jsbeautifier/unpackers/javascriptobfuscator.py | 58 + .../contrib/jsbeautifier/unpackers/myobfuscate.py | 86 + mitmproxy/contrib/jsbeautifier/unpackers/packer.py | 103 + .../contrib/jsbeautifier/unpackers/urlencode.py | 34 + mitmproxy/contrib/tls/__init__.py | 5 + mitmproxy/contrib/tls/_constructs.py | 213 + mitmproxy/contrib/tls/utils.py | 26 + mitmproxy/contrib/wbxml/ASCommandResponse.py | 72 + mitmproxy/contrib/wbxml/ASWBXML.py | 903 + mitmproxy/contrib/wbxml/ASWBXMLByteQueue.py | 103 + mitmproxy/contrib/wbxml/ASWBXMLCodePage.py | 52 + mitmproxy/contrib/wbxml/GlobalTokens.py | 50 + mitmproxy/contrib/wbxml/InvalidDataException.py | 31 + mitmproxy/contrib/wbxml/__init__.py | 0 mitmproxy/controller.py | 143 + mitmproxy/dump.py | 352 + mitmproxy/exceptions.py | 57 + mitmproxy/filt.py | 428 + mitmproxy/flow.py | 1166 + mitmproxy/flow_export.py | 73 + mitmproxy/flow_format_compat.py | 60 + mitmproxy/main.py | 138 + mitmproxy/mitmproxy/__init__.py | 0 mitmproxy/mitmproxy/cmdline.py | 789 - mitmproxy/mitmproxy/console/__init__.py | 744 - mitmproxy/mitmproxy/console/common.py | 444 - mitmproxy/mitmproxy/console/flowdetailview.py | 153 - mitmproxy/mitmproxy/console/flowlist.py | 397 - mitmproxy/mitmproxy/console/flowview.py | 714 - mitmproxy/mitmproxy/console/grideditor.py | 716 - mitmproxy/mitmproxy/console/help.py | 117 - mitmproxy/mitmproxy/console/options.py | 271 - mitmproxy/mitmproxy/console/palettepicker.py | 82 - mitmproxy/mitmproxy/console/palettes.py | 326 - mitmproxy/mitmproxy/console/pathedit.py | 71 - mitmproxy/mitmproxy/console/searchable.py | 93 - mitmproxy/mitmproxy/console/select.py | 120 - mitmproxy/mitmproxy/console/signals.py | 43 - mitmproxy/mitmproxy/console/statusbar.py | 258 - mitmproxy/mitmproxy/console/tabs.py | 70 - mitmproxy/mitmproxy/console/window.py | 90 - mitmproxy/mitmproxy/contentviews.py | 622 - mitmproxy/mitmproxy/contrib/README | 14 - mitmproxy/mitmproxy/contrib/__init__.py | 0 .../mitmproxy/contrib/jsbeautifier/__init__.py | 1153 - .../jsbeautifier/unpackers/README.specs.mkd | 25 - .../contrib/jsbeautifier/unpackers/__init__.py | 66 - .../contrib/jsbeautifier/unpackers/evalbased.py | 39 - .../jsbeautifier/unpackers/javascriptobfuscator.py | 58 - .../contrib/jsbeautifier/unpackers/myobfuscate.py | 86 - .../contrib/jsbeautifier/unpackers/packer.py | 103 - .../contrib/jsbeautifier/unpackers/urlencode.py | 34 - mitmproxy/mitmproxy/contrib/tls/__init__.py | 5 - mitmproxy/mitmproxy/contrib/tls/_constructs.py | 213 - mitmproxy/mitmproxy/contrib/tls/utils.py | 26 - .../mitmproxy/contrib/wbxml/ASCommandResponse.py | 72 - mitmproxy/mitmproxy/contrib/wbxml/ASWBXML.py | 903 - .../mitmproxy/contrib/wbxml/ASWBXMLByteQueue.py | 103 - .../mitmproxy/contrib/wbxml/ASWBXMLCodePage.py | 52 - mitmproxy/mitmproxy/contrib/wbxml/GlobalTokens.py | 50 - .../contrib/wbxml/InvalidDataException.py | 31 - mitmproxy/mitmproxy/contrib/wbxml/__init__.py | 0 mitmproxy/mitmproxy/controller.py | 143 - mitmproxy/mitmproxy/dump.py | 352 - mitmproxy/mitmproxy/exceptions.py | 57 - mitmproxy/mitmproxy/filt.py | 428 - mitmproxy/mitmproxy/flow.py | 1166 - mitmproxy/mitmproxy/flow_export.py | 73 - mitmproxy/mitmproxy/flow_format_compat.py | 60 - mitmproxy/mitmproxy/main.py | 138 - mitmproxy/mitmproxy/models/__init__.py | 16 - mitmproxy/mitmproxy/models/connections.py | 161 - mitmproxy/mitmproxy/models/flow.py | 171 - mitmproxy/mitmproxy/models/http.py | 469 - mitmproxy/mitmproxy/onboarding/__init__.py | 0 mitmproxy/mitmproxy/onboarding/app.py | 93 - .../mitmproxy/onboarding/static/bootstrap.min.css | 1 - .../static/fontawesome/css/font-awesome.css | 1338 - .../static/fontawesome/css/font-awesome.min.css | 4 - .../static/fontawesome/fonts/FontAwesome.otf | Bin 62856 -> 0 bytes .../fontawesome/fonts/fontawesome-webfont.eot | Bin 38205 -> 0 bytes .../fontawesome/fonts/fontawesome-webfont.svg | 414 - .../fontawesome/fonts/fontawesome-webfont.ttf | Bin 80652 -> 0 bytes .../fontawesome/fonts/fontawesome-webfont.woff | Bin 44432 -> 0 bytes .../mitmproxy/onboarding/static/mitmproxy.css | 48 - .../mitmproxy/onboarding/templates/frame.html | 9 - .../mitmproxy/onboarding/templates/index.html | 35 - .../mitmproxy/onboarding/templates/layout.html | 32 - mitmproxy/mitmproxy/platform/__init__.py | 16 - mitmproxy/mitmproxy/platform/linux.py | 14 - mitmproxy/mitmproxy/platform/osx.py | 36 - mitmproxy/mitmproxy/platform/pf.py | 24 - mitmproxy/mitmproxy/platform/windows.py | 432 - mitmproxy/mitmproxy/protocol/__init__.py | 45 - mitmproxy/mitmproxy/protocol/base.py | 198 - mitmproxy/mitmproxy/protocol/http.py | 416 - mitmproxy/mitmproxy/protocol/http1.py | 67 - mitmproxy/mitmproxy/protocol/http2.py | 422 - mitmproxy/mitmproxy/protocol/http_replay.py | 105 - mitmproxy/mitmproxy/protocol/rawtcp.py | 88 - mitmproxy/mitmproxy/protocol/tls.py | 566 - mitmproxy/mitmproxy/proxy/__init__.py | 11 - mitmproxy/mitmproxy/proxy/config.py | 208 - mitmproxy/mitmproxy/proxy/modes/__init__.py | 12 - mitmproxy/mitmproxy/proxy/modes/http_proxy.py | 28 - mitmproxy/mitmproxy/proxy/modes/reverse_proxy.py | 18 - mitmproxy/mitmproxy/proxy/modes/socks_proxy.py | 66 - .../mitmproxy/proxy/modes/transparent_proxy.py | 25 - mitmproxy/mitmproxy/proxy/root_context.py | 139 - mitmproxy/mitmproxy/proxy/server.py | 157 - mitmproxy/mitmproxy/script/__init__.py | 13 - mitmproxy/mitmproxy/script/concurrent.py | 63 - mitmproxy/mitmproxy/script/reloader.py | 46 - mitmproxy/mitmproxy/script/script.py | 100 - mitmproxy/mitmproxy/script/script_context.py | 60 - mitmproxy/mitmproxy/stateobject.py | 52 - mitmproxy/mitmproxy/tnetstring.py | 399 - mitmproxy/mitmproxy/utils.py | 176 - mitmproxy/mitmproxy/version.py | 3 - mitmproxy/mitmproxy/web/__init__.py | 215 - mitmproxy/mitmproxy/web/app.py | 338 - mitmproxy/mitmproxy/web/static/app.css | 421 - mitmproxy/mitmproxy/web/static/app.js | 6271 --- .../web/static/fonts/fontawesome-webfont.eot | Bin 56006 -> 0 bytes .../web/static/fonts/fontawesome-webfont.svg | 520 - .../web/static/fonts/fontawesome-webfont.ttf | Bin 112160 -> 0 bytes .../web/static/fonts/fontawesome-webfont.woff | Bin 65452 -> 0 bytes .../web/static/images/chrome-devtools/LICENSE | 30 - .../images/chrome-devtools/resourceCSSIcon.png | Bin 1005 -> 0 bytes .../chrome-devtools/resourceDocumentIcon.png | Bin 951 -> 0 bytes .../images/chrome-devtools/resourceJSIcon.png | Bin 787 -> 0 bytes .../images/chrome-devtools/resourcePlainIcon.png | Bin 295 -> 0 bytes .../web/static/images/resourceExecutableIcon.png | Bin 853 -> 0 bytes .../web/static/images/resourceFlashIcon.png | Bin 921 -> 0 bytes .../web/static/images/resourceImageIcon.png | Bin 976 -> 0 bytes .../web/static/images/resourceJavaIcon.png | Bin 861 -> 0 bytes .../web/static/images/resourceNotModifiedIcon.png | Bin 1072 -> 0 bytes .../web/static/images/resourceRedirectIcon.png | Bin 1174 -> 0 bytes mitmproxy/mitmproxy/web/static/vendor.css | 8437 ---- mitmproxy/mitmproxy/web/static/vendor.js | 46760 ------------------- mitmproxy/mitmproxy/web/templates/index.html | 14 - .../mitmproxy/webfonts/fontawesome-webfont.eot | Bin 56006 -> 0 bytes .../mitmproxy/webfonts/fontawesome-webfont.svg | 520 - .../mitmproxy/webfonts/fontawesome-webfont.ttf | Bin 112160 -> 0 bytes .../mitmproxy/webfonts/fontawesome-webfont.woff | Bin 65452 -> 0 bytes mitmproxy/models/__init__.py | 16 + mitmproxy/models/connections.py | 161 + mitmproxy/models/flow.py | 171 + mitmproxy/models/http.py | 469 + mitmproxy/onboarding/__init__.py | 0 mitmproxy/onboarding/app.py | 93 + mitmproxy/onboarding/static/bootstrap.min.css | 1 + .../static/fontawesome/css/font-awesome.css | 1338 + .../static/fontawesome/css/font-awesome.min.css | 4 + .../static/fontawesome/fonts/FontAwesome.otf | Bin 0 -> 62856 bytes .../fontawesome/fonts/fontawesome-webfont.eot | Bin 0 -> 38205 bytes .../fontawesome/fonts/fontawesome-webfont.svg | 414 + .../fontawesome/fonts/fontawesome-webfont.ttf | Bin 0 -> 80652 bytes .../fontawesome/fonts/fontawesome-webfont.woff | Bin 0 -> 44432 bytes mitmproxy/onboarding/static/mitmproxy.css | 48 + mitmproxy/onboarding/templates/frame.html | 9 + mitmproxy/onboarding/templates/index.html | 35 + mitmproxy/onboarding/templates/layout.html | 32 + mitmproxy/platform/__init__.py | 16 + mitmproxy/platform/linux.py | 14 + mitmproxy/platform/osx.py | 36 + mitmproxy/platform/pf.py | 24 + mitmproxy/platform/windows.py | 432 + mitmproxy/protocol/__init__.py | 45 + mitmproxy/protocol/base.py | 198 + mitmproxy/protocol/http.py | 416 + mitmproxy/protocol/http1.py | 67 + mitmproxy/protocol/http2.py | 422 + mitmproxy/protocol/http_replay.py | 105 + mitmproxy/protocol/rawtcp.py | 88 + mitmproxy/protocol/tls.py | 566 + mitmproxy/proxy/__init__.py | 11 + mitmproxy/proxy/config.py | 208 + mitmproxy/proxy/modes/__init__.py | 12 + mitmproxy/proxy/modes/http_proxy.py | 28 + mitmproxy/proxy/modes/reverse_proxy.py | 18 + mitmproxy/proxy/modes/socks_proxy.py | 66 + mitmproxy/proxy/modes/transparent_proxy.py | 25 + mitmproxy/proxy/root_context.py | 139 + mitmproxy/proxy/server.py | 157 + mitmproxy/script/__init__.py | 13 + mitmproxy/script/concurrent.py | 63 + mitmproxy/script/reloader.py | 46 + mitmproxy/script/script.py | 100 + mitmproxy/script/script_context.py | 60 + mitmproxy/setup.cfg | 11 - mitmproxy/setup.py | 107 - mitmproxy/stateobject.py | 52 + mitmproxy/tnetstring.py | 399 + mitmproxy/utils.py | 176 + mitmproxy/version.py | 3 + mitmproxy/web/__init__.py | 215 + mitmproxy/web/app.py | 338 + mitmproxy/web/static/app.css | 421 + mitmproxy/web/static/app.js | 6271 +++ mitmproxy/web/static/fonts/fontawesome-webfont.eot | Bin 0 -> 56006 bytes mitmproxy/web/static/fonts/fontawesome-webfont.svg | 520 + mitmproxy/web/static/fonts/fontawesome-webfont.ttf | Bin 0 -> 112160 bytes .../web/static/fonts/fontawesome-webfont.woff | Bin 0 -> 65452 bytes .../web/static/images/chrome-devtools/LICENSE | 30 + .../images/chrome-devtools/resourceCSSIcon.png | Bin 0 -> 1005 bytes .../chrome-devtools/resourceDocumentIcon.png | Bin 0 -> 951 bytes .../images/chrome-devtools/resourceJSIcon.png | Bin 0 -> 787 bytes .../images/chrome-devtools/resourcePlainIcon.png | Bin 0 -> 295 bytes .../web/static/images/resourceExecutableIcon.png | Bin 0 -> 853 bytes mitmproxy/web/static/images/resourceFlashIcon.png | Bin 0 -> 921 bytes mitmproxy/web/static/images/resourceImageIcon.png | Bin 0 -> 976 bytes mitmproxy/web/static/images/resourceJavaIcon.png | Bin 0 -> 861 bytes .../web/static/images/resourceNotModifiedIcon.png | Bin 0 -> 1072 bytes .../web/static/images/resourceRedirectIcon.png | Bin 0 -> 1174 bytes mitmproxy/web/static/vendor.css | 8437 ++++ mitmproxy/web/static/vendor.js | 46760 +++++++++++++++++++ mitmproxy/web/templates/index.html | 14 + mitmproxy/webfonts/fontawesome-webfont.eot | Bin 0 -> 56006 bytes mitmproxy/webfonts/fontawesome-webfont.svg | 520 + mitmproxy/webfonts/fontawesome-webfont.ttf | Bin 0 -> 112160 bytes mitmproxy/webfonts/fontawesome-webfont.woff | Bin 0 -> 65452 bytes netlib/README.rst | 35 - netlib/__init__.py | 1 + netlib/certutils.py | 472 + netlib/encoding.py | 88 + netlib/exceptions.py | 56 + netlib/http/__init__.py | 14 + netlib/http/authentication.py | 167 + netlib/http/cookies.py | 193 + netlib/http/headers.py | 204 + netlib/http/http1/__init__.py | 25 + netlib/http/http1/assemble.py | 104 + netlib/http/http1/read.py | 362 + netlib/http/http2/__init__.py | 6 + netlib/http/http2/connections.py | 426 + netlib/http/message.py | 222 + netlib/http/request.py | 356 + netlib/http/response.py | 116 + netlib/http/status_codes.py | 106 + netlib/http/user_agents.py | 52 + netlib/netlib/__init__.py | 1 - netlib/netlib/certutils.py | 472 - netlib/netlib/encoding.py | 88 - netlib/netlib/exceptions.py | 56 - netlib/netlib/http/__init__.py | 14 - netlib/netlib/http/authentication.py | 167 - netlib/netlib/http/cookies.py | 193 - netlib/netlib/http/headers.py | 204 - netlib/netlib/http/http1/__init__.py | 25 - netlib/netlib/http/http1/assemble.py | 104 - netlib/netlib/http/http1/read.py | 362 - netlib/netlib/http/http2/__init__.py | 6 - netlib/netlib/http/http2/connections.py | 426 - netlib/netlib/http/message.py | 222 - netlib/netlib/http/request.py | 356 - netlib/netlib/http/response.py | 116 - netlib/netlib/http/status_codes.py | 106 - netlib/netlib/http/user_agents.py | 52 - netlib/netlib/odict.py | 193 - netlib/netlib/socks.py | 176 - netlib/netlib/tcp.py | 911 - netlib/netlib/tutils.py | 133 - netlib/netlib/utils.py | 418 - netlib/netlib/version.py | 6 - netlib/netlib/version_check.py | 60 - netlib/netlib/websockets/__init__.py | 2 - netlib/netlib/websockets/frame.py | 316 - netlib/netlib/websockets/protocol.py | 115 - netlib/netlib/wsgi.py | 164 - netlib/odict.py | 193 + netlib/setup.cfg | 2 - netlib/setup.py | 70 - netlib/socks.py | 176 + netlib/tcp.py | 911 + netlib/tutils.py | 133 + netlib/utils.py | 418 + netlib/version.py | 6 + netlib/version_check.py | 60 + netlib/websockets/__init__.py | 2 + netlib/websockets/frame.py | 316 + netlib/websockets/protocol.py | 115 + netlib/wsgi.py | 164 + pathod/.jsbeautifyrc | 22 - pathod/README.rst | 60 - pathod/__init__.py | 0 pathod/app.py | 179 + pathod/language/__init__.py | 113 + pathod/language/actions.py | 126 + pathod/language/base.py | 576 + pathod/language/exceptions.py | 22 + pathod/language/generators.py | 86 + pathod/language/http.py | 381 + pathod/language/http2.py | 299 + pathod/language/message.py | 96 + pathod/language/websockets.py | 241 + pathod/language/writer.py | 67 + pathod/log.py | 83 + pathod/pathoc.py | 534 + pathod/pathoc_cmdline.py | 226 + pathod/pathod.py | 503 + pathod/pathod/__init__.py | 0 pathod/pathod/app.py | 179 - pathod/pathod/language/__init__.py | 113 - pathod/pathod/language/actions.py | 126 - pathod/pathod/language/base.py | 576 - pathod/pathod/language/exceptions.py | 22 - pathod/pathod/language/generators.py | 86 - pathod/pathod/language/http.py | 381 - pathod/pathod/language/http2.py | 299 - pathod/pathod/language/message.py | 96 - pathod/pathod/language/websockets.py | 241 - pathod/pathod/language/writer.py | 67 - pathod/pathod/log.py | 83 - pathod/pathod/pathoc.py | 534 - pathod/pathod/pathoc_cmdline.py | 226 - pathod/pathod/pathod.py | 503 - pathod/pathod/pathod_cmdline.py | 231 - pathod/pathod/protocols/__init__.py | 1 - pathod/pathod/protocols/http.py | 71 - pathod/pathod/protocols/http2.py | 20 - pathod/pathod/protocols/websockets.py | 56 - pathod/pathod/static/bootstrap.min.css | 9 - pathod/pathod/static/bootstrap.min.js | 6 - pathod/pathod/static/jquery-1.7.2.min.js | 4 - pathod/pathod/static/jquery.localscroll-min.js | 9 - pathod/pathod/static/jquery.scrollTo-min.js | 11 - pathod/pathod/static/pathod.css | 56 - pathod/pathod/static/start_quote.png | Bin 376 -> 0 bytes pathod/pathod/static/syntax.css | 120 - pathod/pathod/static/torture.png | Bin 108327 -> 0 bytes pathod/pathod/templates/about.html | 22 - pathod/pathod/templates/docframe.html | 26 - pathod/pathod/templates/docs_lang.html | 196 - pathod/pathod/templates/docs_lang_requests.html | 114 - pathod/pathod/templates/docs_lang_responses.html | 88 - pathod/pathod/templates/docs_lang_websockets.html | 115 - pathod/pathod/templates/docs_libpathod.html | 23 - pathod/pathod/templates/docs_pathoc.html | 211 - pathod/pathod/templates/docs_pathod.html | 172 - pathod/pathod/templates/docs_test.html | 50 - pathod/pathod/templates/download.html | 39 - pathod/pathod/templates/examples_context.html | 24 - pathod/pathod/templates/examples_setup.html | 32 - pathod/pathod/templates/examples_setupall.html | 40 - pathod/pathod/templates/frame.html | 7 - pathod/pathod/templates/index.html | 60 - pathod/pathod/templates/layout.html | 75 - pathod/pathod/templates/log.html | 31 - pathod/pathod/templates/onelog.html | 8 - pathod/pathod/templates/request_preview.html | 44 - pathod/pathod/templates/request_previewform.html | 53 - pathod/pathod/templates/response_preview.html | 44 - pathod/pathod/templates/response_previewform.html | 87 - pathod/pathod/test.py | 103 - pathod/pathod/utils.py | 124 - pathod/pathod/version.py | 3 - pathod/pathod_cmdline.py | 231 + pathod/protocols/__init__.py | 1 + pathod/protocols/http.py | 71 + pathod/protocols/http2.py | 20 + pathod/protocols/websockets.py | 56 + pathod/setup.py | 65 - pathod/static/bootstrap.min.css | 9 + pathod/static/bootstrap.min.js | 6 + pathod/static/jquery-1.7.2.min.js | 4 + pathod/static/jquery.localscroll-min.js | 9 + pathod/static/jquery.scrollTo-min.js | 11 + pathod/static/pathod.css | 56 + pathod/static/start_quote.png | Bin 0 -> 376 bytes pathod/static/syntax.css | 120 + pathod/static/torture.png | Bin 0 -> 108327 bytes pathod/templates/about.html | 22 + pathod/templates/docframe.html | 26 + pathod/templates/docs_lang.html | 196 + pathod/templates/docs_lang_requests.html | 114 + pathod/templates/docs_lang_responses.html | 88 + pathod/templates/docs_lang_websockets.html | 115 + pathod/templates/docs_libpathod.html | 23 + pathod/templates/docs_pathoc.html | 211 + pathod/templates/docs_pathod.html | 172 + pathod/templates/docs_test.html | 50 + pathod/templates/download.html | 39 + pathod/templates/examples_context.html | 24 + pathod/templates/examples_setup.html | 32 + pathod/templates/examples_setupall.html | 40 + pathod/templates/frame.html | 7 + pathod/templates/index.html | 60 + pathod/templates/layout.html | 75 + pathod/templates/log.html | 31 + pathod/templates/onelog.html | 8 + pathod/templates/request_preview.html | 44 + pathod/templates/request_previewform.html | 53 + pathod/templates/response_preview.html | 44 + pathod/templates/response_previewform.html | 87 + pathod/test.py | 103 + pathod/utils.py | 124 + pathod/version.py | 3 + pytest.ini | 3 - requirements.txt | 4 +- setup.cfg | 12 + setup.py | 122 + 429 files changed, 93408 insertions(+), 93724 deletions(-) delete mode 100644 mitmproxy/README.rst create mode 100644 mitmproxy/__init__.py create mode 100644 mitmproxy/cmdline.py create mode 100644 mitmproxy/console/__init__.py create mode 100644 mitmproxy/console/common.py create mode 100644 mitmproxy/console/flowdetailview.py create mode 100644 mitmproxy/console/flowlist.py create mode 100644 mitmproxy/console/flowview.py create mode 100644 mitmproxy/console/grideditor.py create mode 100644 mitmproxy/console/help.py create mode 100644 mitmproxy/console/options.py create mode 100644 mitmproxy/console/palettepicker.py create mode 100644 mitmproxy/console/palettes.py create mode 100644 mitmproxy/console/pathedit.py create mode 100644 mitmproxy/console/searchable.py create mode 100644 mitmproxy/console/select.py create mode 100644 mitmproxy/console/signals.py create mode 100644 mitmproxy/console/statusbar.py create mode 100644 mitmproxy/console/tabs.py create mode 100644 mitmproxy/console/window.py create mode 100644 mitmproxy/contentviews.py create mode 100644 mitmproxy/contrib/README create mode 100644 mitmproxy/contrib/__init__.py create mode 100644 mitmproxy/contrib/jsbeautifier/__init__.py create mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd create mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/__init__.py create mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py create mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py create mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py create mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/packer.py create mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/urlencode.py create mode 100644 mitmproxy/contrib/tls/__init__.py create mode 100644 mitmproxy/contrib/tls/_constructs.py create mode 100644 mitmproxy/contrib/tls/utils.py create mode 100644 mitmproxy/contrib/wbxml/ASCommandResponse.py create mode 100644 mitmproxy/contrib/wbxml/ASWBXML.py create mode 100644 mitmproxy/contrib/wbxml/ASWBXMLByteQueue.py create mode 100644 mitmproxy/contrib/wbxml/ASWBXMLCodePage.py create mode 100644 mitmproxy/contrib/wbxml/GlobalTokens.py create mode 100644 mitmproxy/contrib/wbxml/InvalidDataException.py create mode 100644 mitmproxy/contrib/wbxml/__init__.py create mode 100644 mitmproxy/controller.py create mode 100644 mitmproxy/dump.py create mode 100644 mitmproxy/exceptions.py create mode 100644 mitmproxy/filt.py create mode 100644 mitmproxy/flow.py create mode 100644 mitmproxy/flow_export.py create mode 100644 mitmproxy/flow_format_compat.py create mode 100644 mitmproxy/main.py delete mode 100644 mitmproxy/mitmproxy/__init__.py delete mode 100644 mitmproxy/mitmproxy/cmdline.py delete mode 100644 mitmproxy/mitmproxy/console/__init__.py delete mode 100644 mitmproxy/mitmproxy/console/common.py delete mode 100644 mitmproxy/mitmproxy/console/flowdetailview.py delete mode 100644 mitmproxy/mitmproxy/console/flowlist.py delete mode 100644 mitmproxy/mitmproxy/console/flowview.py delete mode 100644 mitmproxy/mitmproxy/console/grideditor.py delete mode 100644 mitmproxy/mitmproxy/console/help.py delete mode 100644 mitmproxy/mitmproxy/console/options.py delete mode 100644 mitmproxy/mitmproxy/console/palettepicker.py delete mode 100644 mitmproxy/mitmproxy/console/palettes.py delete mode 100644 mitmproxy/mitmproxy/console/pathedit.py delete mode 100644 mitmproxy/mitmproxy/console/searchable.py delete mode 100644 mitmproxy/mitmproxy/console/select.py delete mode 100644 mitmproxy/mitmproxy/console/signals.py delete mode 100644 mitmproxy/mitmproxy/console/statusbar.py delete mode 100644 mitmproxy/mitmproxy/console/tabs.py delete mode 100644 mitmproxy/mitmproxy/console/window.py delete mode 100644 mitmproxy/mitmproxy/contentviews.py delete mode 100644 mitmproxy/mitmproxy/contrib/README delete mode 100644 mitmproxy/mitmproxy/contrib/__init__.py delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/__init__.py delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/unpackers/__init__.py delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/unpackers/packer.py delete mode 100644 mitmproxy/mitmproxy/contrib/jsbeautifier/unpackers/urlencode.py delete mode 100644 mitmproxy/mitmproxy/contrib/tls/__init__.py delete mode 100644 mitmproxy/mitmproxy/contrib/tls/_constructs.py delete mode 100644 mitmproxy/mitmproxy/contrib/tls/utils.py delete mode 100644 mitmproxy/mitmproxy/contrib/wbxml/ASCommandResponse.py delete mode 100644 mitmproxy/mitmproxy/contrib/wbxml/ASWBXML.py delete mode 100644 mitmproxy/mitmproxy/contrib/wbxml/ASWBXMLByteQueue.py delete mode 100644 mitmproxy/mitmproxy/contrib/wbxml/ASWBXMLCodePage.py delete mode 100644 mitmproxy/mitmproxy/contrib/wbxml/GlobalTokens.py delete mode 100644 mitmproxy/mitmproxy/contrib/wbxml/InvalidDataException.py delete mode 100644 mitmproxy/mitmproxy/contrib/wbxml/__init__.py delete mode 100644 mitmproxy/mitmproxy/controller.py delete mode 100644 mitmproxy/mitmproxy/dump.py delete mode 100644 mitmproxy/mitmproxy/exceptions.py delete mode 100644 mitmproxy/mitmproxy/filt.py delete mode 100644 mitmproxy/mitmproxy/flow.py delete mode 100644 mitmproxy/mitmproxy/flow_export.py delete mode 100644 mitmproxy/mitmproxy/flow_format_compat.py delete mode 100644 mitmproxy/mitmproxy/main.py delete mode 100644 mitmproxy/mitmproxy/models/__init__.py delete mode 100644 mitmproxy/mitmproxy/models/connections.py delete mode 100644 mitmproxy/mitmproxy/models/flow.py delete mode 100644 mitmproxy/mitmproxy/models/http.py delete mode 100644 mitmproxy/mitmproxy/onboarding/__init__.py delete mode 100644 mitmproxy/mitmproxy/onboarding/app.py delete mode 100644 mitmproxy/mitmproxy/onboarding/static/bootstrap.min.css delete mode 100644 mitmproxy/mitmproxy/onboarding/static/fontawesome/css/font-awesome.css delete mode 100644 mitmproxy/mitmproxy/onboarding/static/fontawesome/css/font-awesome.min.css delete mode 100644 mitmproxy/mitmproxy/onboarding/static/fontawesome/fonts/FontAwesome.otf delete mode 100644 mitmproxy/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.eot delete mode 100644 mitmproxy/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.svg delete mode 100644 mitmproxy/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.ttf delete mode 100644 mitmproxy/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.woff delete mode 100644 mitmproxy/mitmproxy/onboarding/static/mitmproxy.css delete mode 100644 mitmproxy/mitmproxy/onboarding/templates/frame.html delete mode 100644 mitmproxy/mitmproxy/onboarding/templates/index.html delete mode 100644 mitmproxy/mitmproxy/onboarding/templates/layout.html delete mode 100644 mitmproxy/mitmproxy/platform/__init__.py delete mode 100644 mitmproxy/mitmproxy/platform/linux.py delete mode 100644 mitmproxy/mitmproxy/platform/osx.py delete mode 100644 mitmproxy/mitmproxy/platform/pf.py delete mode 100644 mitmproxy/mitmproxy/platform/windows.py delete mode 100644 mitmproxy/mitmproxy/protocol/__init__.py delete mode 100644 mitmproxy/mitmproxy/protocol/base.py delete mode 100644 mitmproxy/mitmproxy/protocol/http.py delete mode 100644 mitmproxy/mitmproxy/protocol/http1.py delete mode 100644 mitmproxy/mitmproxy/protocol/http2.py delete mode 100644 mitmproxy/mitmproxy/protocol/http_replay.py delete mode 100644 mitmproxy/mitmproxy/protocol/rawtcp.py delete mode 100644 mitmproxy/mitmproxy/protocol/tls.py delete mode 100644 mitmproxy/mitmproxy/proxy/__init__.py delete mode 100644 mitmproxy/mitmproxy/proxy/config.py delete mode 100644 mitmproxy/mitmproxy/proxy/modes/__init__.py delete mode 100644 mitmproxy/mitmproxy/proxy/modes/http_proxy.py delete mode 100644 mitmproxy/mitmproxy/proxy/modes/reverse_proxy.py delete mode 100644 mitmproxy/mitmproxy/proxy/modes/socks_proxy.py delete mode 100644 mitmproxy/mitmproxy/proxy/modes/transparent_proxy.py delete mode 100644 mitmproxy/mitmproxy/proxy/root_context.py delete mode 100644 mitmproxy/mitmproxy/proxy/server.py delete mode 100644 mitmproxy/mitmproxy/script/__init__.py delete mode 100644 mitmproxy/mitmproxy/script/concurrent.py delete mode 100644 mitmproxy/mitmproxy/script/reloader.py delete mode 100644 mitmproxy/mitmproxy/script/script.py delete mode 100644 mitmproxy/mitmproxy/script/script_context.py delete mode 100644 mitmproxy/mitmproxy/stateobject.py delete mode 100644 mitmproxy/mitmproxy/tnetstring.py delete mode 100644 mitmproxy/mitmproxy/utils.py delete mode 100644 mitmproxy/mitmproxy/version.py delete mode 100644 mitmproxy/mitmproxy/web/__init__.py delete mode 100644 mitmproxy/mitmproxy/web/app.py delete mode 100644 mitmproxy/mitmproxy/web/static/app.css delete mode 100644 mitmproxy/mitmproxy/web/static/app.js delete mode 100644 mitmproxy/mitmproxy/web/static/fonts/fontawesome-webfont.eot delete mode 100644 mitmproxy/mitmproxy/web/static/fonts/fontawesome-webfont.svg delete mode 100644 mitmproxy/mitmproxy/web/static/fonts/fontawesome-webfont.ttf delete mode 100644 mitmproxy/mitmproxy/web/static/fonts/fontawesome-webfont.woff delete mode 100644 mitmproxy/mitmproxy/web/static/images/chrome-devtools/LICENSE delete mode 100644 mitmproxy/mitmproxy/web/static/images/chrome-devtools/resourceCSSIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/chrome-devtools/resourceDocumentIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/chrome-devtools/resourceJSIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/chrome-devtools/resourcePlainIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/resourceExecutableIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/resourceFlashIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/resourceImageIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/resourceJavaIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/resourceNotModifiedIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/images/resourceRedirectIcon.png delete mode 100644 mitmproxy/mitmproxy/web/static/vendor.css delete mode 100644 mitmproxy/mitmproxy/web/static/vendor.js delete mode 100644 mitmproxy/mitmproxy/web/templates/index.html delete mode 100644 mitmproxy/mitmproxy/webfonts/fontawesome-webfont.eot delete mode 100644 mitmproxy/mitmproxy/webfonts/fontawesome-webfont.svg delete mode 100644 mitmproxy/mitmproxy/webfonts/fontawesome-webfont.ttf delete mode 100644 mitmproxy/mitmproxy/webfonts/fontawesome-webfont.woff create mode 100644 mitmproxy/models/__init__.py create mode 100644 mitmproxy/models/connections.py create mode 100644 mitmproxy/models/flow.py create mode 100644 mitmproxy/models/http.py create mode 100644 mitmproxy/onboarding/__init__.py create mode 100644 mitmproxy/onboarding/app.py create mode 100644 mitmproxy/onboarding/static/bootstrap.min.css create mode 100644 mitmproxy/onboarding/static/fontawesome/css/font-awesome.css create mode 100644 mitmproxy/onboarding/static/fontawesome/css/font-awesome.min.css create mode 100644 mitmproxy/onboarding/static/fontawesome/fonts/FontAwesome.otf create mode 100644 mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.eot create mode 100644 mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.svg create mode 100644 mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.ttf create mode 100644 mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.woff create mode 100644 mitmproxy/onboarding/static/mitmproxy.css create mode 100644 mitmproxy/onboarding/templates/frame.html create mode 100644 mitmproxy/onboarding/templates/index.html create mode 100644 mitmproxy/onboarding/templates/layout.html create mode 100644 mitmproxy/platform/__init__.py create mode 100644 mitmproxy/platform/linux.py create mode 100644 mitmproxy/platform/osx.py create mode 100644 mitmproxy/platform/pf.py create mode 100644 mitmproxy/platform/windows.py create mode 100644 mitmproxy/protocol/__init__.py create mode 100644 mitmproxy/protocol/base.py create mode 100644 mitmproxy/protocol/http.py create mode 100644 mitmproxy/protocol/http1.py create mode 100644 mitmproxy/protocol/http2.py create mode 100644 mitmproxy/protocol/http_replay.py create mode 100644 mitmproxy/protocol/rawtcp.py create mode 100644 mitmproxy/protocol/tls.py create mode 100644 mitmproxy/proxy/__init__.py create mode 100644 mitmproxy/proxy/config.py create mode 100644 mitmproxy/proxy/modes/__init__.py create mode 100644 mitmproxy/proxy/modes/http_proxy.py create mode 100644 mitmproxy/proxy/modes/reverse_proxy.py create mode 100644 mitmproxy/proxy/modes/socks_proxy.py create mode 100644 mitmproxy/proxy/modes/transparent_proxy.py create mode 100644 mitmproxy/proxy/root_context.py create mode 100644 mitmproxy/proxy/server.py create mode 100644 mitmproxy/script/__init__.py create mode 100644 mitmproxy/script/concurrent.py create mode 100644 mitmproxy/script/reloader.py create mode 100644 mitmproxy/script/script.py create mode 100644 mitmproxy/script/script_context.py delete mode 100644 mitmproxy/setup.cfg delete mode 100644 mitmproxy/setup.py create mode 100644 mitmproxy/stateobject.py create mode 100644 mitmproxy/tnetstring.py create mode 100644 mitmproxy/utils.py create mode 100644 mitmproxy/version.py create mode 100644 mitmproxy/web/__init__.py create mode 100644 mitmproxy/web/app.py create mode 100644 mitmproxy/web/static/app.css create mode 100644 mitmproxy/web/static/app.js create mode 100644 mitmproxy/web/static/fonts/fontawesome-webfont.eot create mode 100644 mitmproxy/web/static/fonts/fontawesome-webfont.svg create mode 100644 mitmproxy/web/static/fonts/fontawesome-webfont.ttf create mode 100644 mitmproxy/web/static/fonts/fontawesome-webfont.woff create mode 100644 mitmproxy/web/static/images/chrome-devtools/LICENSE create mode 100644 mitmproxy/web/static/images/chrome-devtools/resourceCSSIcon.png create mode 100644 mitmproxy/web/static/images/chrome-devtools/resourceDocumentIcon.png create mode 100644 mitmproxy/web/static/images/chrome-devtools/resourceJSIcon.png create mode 100644 mitmproxy/web/static/images/chrome-devtools/resourcePlainIcon.png create mode 100644 mitmproxy/web/static/images/resourceExecutableIcon.png create mode 100644 mitmproxy/web/static/images/resourceFlashIcon.png create mode 100644 mitmproxy/web/static/images/resourceImageIcon.png create mode 100644 mitmproxy/web/static/images/resourceJavaIcon.png create mode 100644 mitmproxy/web/static/images/resourceNotModifiedIcon.png create mode 100644 mitmproxy/web/static/images/resourceRedirectIcon.png create mode 100644 mitmproxy/web/static/vendor.css create mode 100644 mitmproxy/web/static/vendor.js create mode 100644 mitmproxy/web/templates/index.html create mode 100644 mitmproxy/webfonts/fontawesome-webfont.eot create mode 100644 mitmproxy/webfonts/fontawesome-webfont.svg create mode 100644 mitmproxy/webfonts/fontawesome-webfont.ttf create mode 100644 mitmproxy/webfonts/fontawesome-webfont.woff delete mode 100644 netlib/README.rst create mode 100644 netlib/__init__.py create mode 100644 netlib/certutils.py create mode 100644 netlib/encoding.py create mode 100644 netlib/exceptions.py create mode 100644 netlib/http/__init__.py create mode 100644 netlib/http/authentication.py create mode 100644 netlib/http/cookies.py create mode 100644 netlib/http/headers.py create mode 100644 netlib/http/http1/__init__.py create mode 100644 netlib/http/http1/assemble.py create mode 100644 netlib/http/http1/read.py create mode 100644 netlib/http/http2/__init__.py create mode 100644 netlib/http/http2/connections.py create mode 100644 netlib/http/message.py create mode 100644 netlib/http/request.py create mode 100644 netlib/http/response.py create mode 100644 netlib/http/status_codes.py create mode 100644 netlib/http/user_agents.py delete mode 100644 netlib/netlib/__init__.py delete mode 100644 netlib/netlib/certutils.py delete mode 100644 netlib/netlib/encoding.py delete mode 100644 netlib/netlib/exceptions.py delete mode 100644 netlib/netlib/http/__init__.py delete mode 100644 netlib/netlib/http/authentication.py delete mode 100644 netlib/netlib/http/cookies.py delete mode 100644 netlib/netlib/http/headers.py delete mode 100644 netlib/netlib/http/http1/__init__.py delete mode 100644 netlib/netlib/http/http1/assemble.py delete mode 100644 netlib/netlib/http/http1/read.py delete mode 100644 netlib/netlib/http/http2/__init__.py delete mode 100644 netlib/netlib/http/http2/connections.py delete mode 100644 netlib/netlib/http/message.py delete mode 100644 netlib/netlib/http/request.py delete mode 100644 netlib/netlib/http/response.py delete mode 100644 netlib/netlib/http/status_codes.py delete mode 100644 netlib/netlib/http/user_agents.py delete mode 100644 netlib/netlib/odict.py delete mode 100644 netlib/netlib/socks.py delete mode 100644 netlib/netlib/tcp.py delete mode 100644 netlib/netlib/tutils.py delete mode 100644 netlib/netlib/utils.py delete mode 100644 netlib/netlib/version.py delete mode 100644 netlib/netlib/version_check.py delete mode 100644 netlib/netlib/websockets/__init__.py delete mode 100644 netlib/netlib/websockets/frame.py delete mode 100644 netlib/netlib/websockets/protocol.py delete mode 100644 netlib/netlib/wsgi.py create mode 100644 netlib/odict.py delete mode 100644 netlib/setup.cfg delete mode 100644 netlib/setup.py create mode 100644 netlib/socks.py create mode 100644 netlib/tcp.py create mode 100644 netlib/tutils.py create mode 100644 netlib/utils.py create mode 100644 netlib/version.py create mode 100644 netlib/version_check.py create mode 100644 netlib/websockets/__init__.py create mode 100644 netlib/websockets/frame.py create mode 100644 netlib/websockets/protocol.py create mode 100644 netlib/wsgi.py delete mode 100644 pathod/.jsbeautifyrc delete mode 100644 pathod/README.rst create mode 100644 pathod/__init__.py create mode 100644 pathod/app.py create mode 100644 pathod/language/__init__.py create mode 100644 pathod/language/actions.py create mode 100644 pathod/language/base.py create mode 100644 pathod/language/exceptions.py create mode 100644 pathod/language/generators.py create mode 100644 pathod/language/http.py create mode 100644 pathod/language/http2.py create mode 100644 pathod/language/message.py create mode 100644 pathod/language/websockets.py create mode 100644 pathod/language/writer.py create mode 100644 pathod/log.py create mode 100644 pathod/pathoc.py create mode 100644 pathod/pathoc_cmdline.py create mode 100644 pathod/pathod.py delete mode 100644 pathod/pathod/__init__.py delete mode 100644 pathod/pathod/app.py delete mode 100644 pathod/pathod/language/__init__.py delete mode 100644 pathod/pathod/language/actions.py delete mode 100644 pathod/pathod/language/base.py delete mode 100644 pathod/pathod/language/exceptions.py delete mode 100644 pathod/pathod/language/generators.py delete mode 100644 pathod/pathod/language/http.py delete mode 100644 pathod/pathod/language/http2.py delete mode 100644 pathod/pathod/language/message.py delete mode 100644 pathod/pathod/language/websockets.py delete mode 100644 pathod/pathod/language/writer.py delete mode 100644 pathod/pathod/log.py delete mode 100644 pathod/pathod/pathoc.py delete mode 100644 pathod/pathod/pathoc_cmdline.py delete mode 100644 pathod/pathod/pathod.py delete mode 100644 pathod/pathod/pathod_cmdline.py delete mode 100644 pathod/pathod/protocols/__init__.py delete mode 100644 pathod/pathod/protocols/http.py delete mode 100644 pathod/pathod/protocols/http2.py delete mode 100644 pathod/pathod/protocols/websockets.py delete mode 100644 pathod/pathod/static/bootstrap.min.css delete mode 100644 pathod/pathod/static/bootstrap.min.js delete mode 100644 pathod/pathod/static/jquery-1.7.2.min.js delete mode 100644 pathod/pathod/static/jquery.localscroll-min.js delete mode 100644 pathod/pathod/static/jquery.scrollTo-min.js delete mode 100644 pathod/pathod/static/pathod.css delete mode 100644 pathod/pathod/static/start_quote.png delete mode 100644 pathod/pathod/static/syntax.css delete mode 100644 pathod/pathod/static/torture.png delete mode 100644 pathod/pathod/templates/about.html delete mode 100644 pathod/pathod/templates/docframe.html delete mode 100644 pathod/pathod/templates/docs_lang.html delete mode 100644 pathod/pathod/templates/docs_lang_requests.html delete mode 100644 pathod/pathod/templates/docs_lang_responses.html delete mode 100644 pathod/pathod/templates/docs_lang_websockets.html delete mode 100644 pathod/pathod/templates/docs_libpathod.html delete mode 100644 pathod/pathod/templates/docs_pathoc.html delete mode 100644 pathod/pathod/templates/docs_pathod.html delete mode 100644 pathod/pathod/templates/docs_test.html delete mode 100644 pathod/pathod/templates/download.html delete mode 100644 pathod/pathod/templates/examples_context.html delete mode 100644 pathod/pathod/templates/examples_setup.html delete mode 100644 pathod/pathod/templates/examples_setupall.html delete mode 100644 pathod/pathod/templates/frame.html delete mode 100644 pathod/pathod/templates/index.html delete mode 100644 pathod/pathod/templates/layout.html delete mode 100644 pathod/pathod/templates/log.html delete mode 100644 pathod/pathod/templates/onelog.html delete mode 100644 pathod/pathod/templates/request_preview.html delete mode 100644 pathod/pathod/templates/request_previewform.html delete mode 100644 pathod/pathod/templates/response_preview.html delete mode 100644 pathod/pathod/templates/response_previewform.html delete mode 100644 pathod/pathod/test.py delete mode 100644 pathod/pathod/utils.py delete mode 100644 pathod/pathod/version.py create mode 100644 pathod/pathod_cmdline.py create mode 100644 pathod/protocols/__init__.py create mode 100644 pathod/protocols/http.py create mode 100644 pathod/protocols/http2.py create mode 100644 pathod/protocols/websockets.py delete mode 100644 pathod/setup.py create mode 100644 pathod/static/bootstrap.min.css create mode 100644 pathod/static/bootstrap.min.js create mode 100644 pathod/static/jquery-1.7.2.min.js create mode 100644 pathod/static/jquery.localscroll-min.js create mode 100644 pathod/static/jquery.scrollTo-min.js create mode 100644 pathod/static/pathod.css create mode 100644 pathod/static/start_quote.png create mode 100644 pathod/static/syntax.css create mode 100644 pathod/static/torture.png create mode 100644 pathod/templates/about.html create mode 100644 pathod/templates/docframe.html create mode 100644 pathod/templates/docs_lang.html create mode 100644 pathod/templates/docs_lang_requests.html create mode 100644 pathod/templates/docs_lang_responses.html create mode 100644 pathod/templates/docs_lang_websockets.html create mode 100644 pathod/templates/docs_libpathod.html create mode 100644 pathod/templates/docs_pathoc.html create mode 100644 pathod/templates/docs_pathod.html create mode 100644 pathod/templates/docs_test.html create mode 100644 pathod/templates/download.html create mode 100644 pathod/templates/examples_context.html create mode 100644 pathod/templates/examples_setup.html create mode 100644 pathod/templates/examples_setupall.html create mode 100644 pathod/templates/frame.html create mode 100644 pathod/templates/index.html create mode 100644 pathod/templates/layout.html create mode 100644 pathod/templates/log.html create mode 100644 pathod/templates/onelog.html create mode 100644 pathod/templates/request_preview.html create mode 100644 pathod/templates/request_previewform.html create mode 100644 pathod/templates/response_preview.html create mode 100644 pathod/templates/response_previewform.html create mode 100644 pathod/test.py create mode 100644 pathod/utils.py create mode 100644 pathod/version.py delete mode 100644 pytest.ini create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/mitmproxy/README.rst b/mitmproxy/README.rst deleted file mode 100644 index 8ef92409..00000000 --- a/mitmproxy/README.rst +++ /dev/null @@ -1,73 +0,0 @@ -|travis| |coveralls| |downloads| |latest_release| |python_versions| - -``mitmproxy`` is an interactive, SSL/TLS-capable man-in-the-middle proxy for HTTP -with a console interface. - -``mitmdump`` is the command-line version of mitmproxy. Think tcpdump for HTTP. - - -Features --------- - -- Intercept HTTP requests and responses and modify them on the fly. -- Save complete HTTP conversations for later replay and analysis. -- Replay the client-side of an HTTP conversations. -- Replay HTTP responses of a previously recorded server. -- Reverse proxy mode to forward traffic to a specified server. -- Transparent proxy mode on OSX and Linux. -- Make scripted changes to HTTP traffic using Python. -- SSL/TLS certificates for interception are generated on the fly. -- And much, much more. - -``mitmproxy`` is tested and developed on Mac OSX and Linux. -On Windows, only mitmdump is supported, which does not have a graphical user interface. - - -Documentation & Help --------------------- - -Documentation, tutorials and distribution packages can be found on the -mitmproxy website. - -|mitmproxy_site| - -Installation Instructions are available in the documentation. - -|mitmproxy_docs| - -You can join our developer chat on Slack. - -|slack| - - -.. |mitmproxy_site| image:: https://shields.mitmproxy.org/api/https%3A%2F%2F-mitmproxy.org-blue.svg - :target: https://mitmproxy.org/ - :alt: mitmproxy.org - -.. |mitmproxy_docs| image:: https://readthedocs.org/projects/mitmproxy/badge/ - :target: http://docs.mitmproxy.org/en/latest/ - :alt: mitmproxy documentation - -.. |slack| image:: http://slack.mitmproxy.org/badge.svg - :target: http://slack.mitmproxy.org/ - :alt: Slack Developer Chat - -.. |travis| image:: https://shields.mitmproxy.org/travis/mitmproxy/mitmproxy/master.svg - :target: https://travis-ci.org/mitmproxy/mitmproxy - :alt: Build Status - -.. |coveralls| image:: https://shields.mitmproxy.org/coveralls/mitmproxy/mitmproxy/master.svg - :target: https://coveralls.io/r/mitmproxy/mitmproxy - :alt: Coverage Status - -.. |downloads| image:: https://shields.mitmproxy.org/pypi/dm/mitmproxy.svg?color=orange - :target: https://pypi.python.org/pypi/mitmproxy - :alt: Downloads - -.. |latest_release| image:: https://shields.mitmproxy.org/pypi/v/mitmproxy.svg - :target: https://pypi.python.org/pypi/mitmproxy - :alt: Latest Version - -.. |python_versions| image:: https://shields.mitmproxy.org/pypi/pyversions/mitmproxy.svg - :target: https://pypi.python.org/pypi/mitmproxy - :alt: Supported Python versions diff --git a/mitmproxy/__init__.py b/mitmproxy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mitmproxy/cmdline.py b/mitmproxy/cmdline.py new file mode 100644 index 00000000..fedd4f13 --- /dev/null +++ b/mitmproxy/cmdline.py @@ -0,0 +1,789 @@ +from __future__ import absolute_import +import os +import re + +import configargparse + +from netlib.tcp import Address, sslversion_choices +import netlib.utils +from . import filt, utils, version +from .proxy import config + +APP_HOST = "mitm.it" +APP_PORT = 80 + + +class ParseException(Exception): + pass + + +def _parse_hook(s): + sep, rem = s[0], s[1:] + parts = rem.split(sep, 2) + if len(parts) == 2: + patt = ".*" + a, b = parts + elif len(parts) == 3: + patt, a, b = parts + else: + raise ParseException( + "Malformed hook specifier - too few clauses: %s" % s + ) + + if not a: + raise ParseException("Empty clause: %s" % str(patt)) + + if not filt.parse(patt): + raise ParseException("Malformed filter pattern: %s" % patt) + + return patt, a, b + + +def parse_replace_hook(s): + """ + Returns a (pattern, regex, replacement) tuple. + + The general form for a replacement hook is as follows: + + /patt/regex/replacement + + The first character specifies the separator. Example: + + :~q:foo:bar + + If only two clauses are specified, the pattern is set to match + universally (i.e. ".*"). Example: + + /foo/bar/ + + Clauses are parsed from left to right. Extra separators are taken to be + part of the final clause. For instance, the replacement clause below is + "foo/bar/": + + /one/two/foo/bar/ + + Checks that pattern and regex are both well-formed. Raises + ParseException on error. + """ + patt, regex, replacement = _parse_hook(s) + try: + re.compile(regex) + except re.error as e: + raise ParseException("Malformed replacement regex: %s" % str(e.message)) + return patt, regex, replacement + + +def parse_setheader(s): + """ + Returns a (pattern, header, value) tuple. + + The general form for a replacement hook is as follows: + + /patt/header/value + + The first character specifies the separator. Example: + + :~q:foo:bar + + If only two clauses are specified, the pattern is set to match + universally (i.e. ".*"). Example: + + /foo/bar/ + + Clauses are parsed from left to right. Extra separators are taken to be + part of the final clause. For instance, the value clause below is + "foo/bar/": + + /one/two/foo/bar/ + + Checks that pattern and regex are both well-formed. Raises + ParseException on error. + """ + return _parse_hook(s) + + +def parse_server_spec(url): + try: + p = netlib.utils.parse_url(url) + if p[0] not in ("http", "https"): + raise ValueError() + except ValueError: + raise configargparse.ArgumentTypeError( + "Invalid server specification: %s" % url + ) + + address = Address(p[1:3]) + scheme = p[0].lower() + return config.ServerSpec(scheme, address) + + +def get_common_options(options): + stickycookie, stickyauth = None, None + if options.stickycookie_filt: + stickycookie = options.stickycookie_filt + + if options.stickyauth_filt: + stickyauth = options.stickyauth_filt + + stream_large_bodies = utils.parse_size(options.stream_large_bodies) + + reps = [] + for i in options.replace: + try: + p = parse_replace_hook(i) + except ParseException as e: + raise configargparse.ArgumentTypeError(e.message) + reps.append(p) + for i in options.replace_file: + try: + patt, rex, path = parse_replace_hook(i) + except ParseException as e: + raise configargparse.ArgumentTypeError(e.message) + try: + v = open(path, "rb").read() + except IOError as e: + raise configargparse.ArgumentTypeError( + "Could not read replace file: %s" % path + ) + reps.append((patt, rex, v)) + + setheaders = [] + for i in options.setheader: + try: + p = parse_setheader(i) + except ParseException as e: + raise configargparse.ArgumentTypeError(e.message) + setheaders.append(p) + + return dict( + app=options.app, + app_host=options.app_host, + app_port=options.app_port, + + anticache=options.anticache, + anticomp=options.anticomp, + client_replay=options.client_replay, + kill=options.kill, + no_server=options.no_server, + refresh_server_playback=not options.norefresh, + rheaders=options.rheaders, + rfile=options.rfile, + replacements=reps, + setheaders=setheaders, + server_replay=options.server_replay, + scripts=options.scripts, + stickycookie=stickycookie, + stickyauth=stickyauth, + stream_large_bodies=stream_large_bodies, + showhost=options.showhost, + outfile=options.outfile, + verbosity=options.verbose, + nopop=options.nopop, + replay_ignore_content=options.replay_ignore_content, + replay_ignore_params=options.replay_ignore_params, + replay_ignore_payload_params=options.replay_ignore_payload_params, + replay_ignore_host=options.replay_ignore_host + ) + + +def basic_options(parser): + parser.add_argument( + '--version', + action='version', + version="%(prog)s" + " " + version.VERSION + ) + parser.add_argument( + '--shortversion', + action='version', + help="show program's short version number and exit", + version=version.VERSION + ) + parser.add_argument( + "--anticache", + action="store_true", dest="anticache", default=False, + + help=""" + Strip out request headers that might cause the server to return + 304-not-modified. + """ + ) + parser.add_argument( + "--cadir", + action="store", type=str, dest="cadir", default=config.CA_DIR, + help="Location of the default mitmproxy CA files. (%s)" % config.CA_DIR + ) + parser.add_argument( + "--host", + action="store_true", dest="showhost", default=False, + help="Use the Host header to construct URLs for display." + ) + parser.add_argument( + "-q", "--quiet", + action="store_true", dest="quiet", + help="Quiet." + ) + parser.add_argument( + "-r", "--read-flows", + action="store", dest="rfile", default=None, + help="Read flows from file." + ) + parser.add_argument( + "-s", "--script", + action="append", type=str, dest="scripts", default=[], + metavar='"script.py --bar"', + help=""" + Run a script. Surround with quotes to pass script arguments. Can be + passed multiple times. + """ + ) + parser.add_argument( + "-t", "--stickycookie", + action="store", + dest="stickycookie_filt", + default=None, + metavar="FILTER", + help="Set sticky cookie filter. Matched against requests." + ) + parser.add_argument( + "-u", "--stickyauth", + action="store", dest="stickyauth_filt", default=None, metavar="FILTER", + help="Set sticky auth filter. Matched against requests." + ) + parser.add_argument( + "-v", "--verbose", + action="store_const", dest="verbose", default=1, const=2, + help="Increase event log verbosity." + ) + outfile = parser.add_mutually_exclusive_group() + outfile.add_argument( + "-w", "--wfile", + action="store", dest="outfile", type=lambda f: (f, "wb"), + help="Write flows to file." + ) + outfile.add_argument( + "-a", "--afile", + action="store", dest="outfile", type=lambda f: (f, "ab"), + help="Append flows to file." + ) + parser.add_argument( + "-z", "--anticomp", + action="store_true", dest="anticomp", default=False, + help="Try to convince servers to send us un-compressed data." + ) + parser.add_argument( + "-Z", "--body-size-limit", + action="store", dest="body_size_limit", default=None, + metavar="SIZE", + help="Byte size limit of HTTP request and response bodies." + " Understands k/m/g suffixes, i.e. 3m for 3 megabytes." + ) + parser.add_argument( + "--stream", + action="store", dest="stream_large_bodies", default=None, + metavar="SIZE", + help=""" + Stream data to the client if response body exceeds the given + threshold. If streamed, the body will not be stored in any way. + Understands k/m/g suffixes, i.e. 3m for 3 megabytes. + """ + ) + + +def proxy_modes(parser): + group = parser.add_argument_group("Proxy Modes").add_mutually_exclusive_group() + group.add_argument( + "-R", "--reverse", + action="store", + type=parse_server_spec, + dest="reverse_proxy", + default=None, + help=""" + Forward all requests to upstream HTTP server: + http[s][2http[s]]://host[:port] + """ + ) + group.add_argument( + "--socks", + action="store_true", dest="socks_proxy", default=False, + help="Set SOCKS5 proxy mode." + ) + group.add_argument( + "-T", "--transparent", + action="store_true", dest="transparent_proxy", default=False, + help="Set transparent proxy mode." + ) + group.add_argument( + "-U", "--upstream", + action="store", + type=parse_server_spec, + dest="upstream_proxy", + default=None, + help="Forward all requests to upstream proxy server: http://host[:port]" + ) + + +def proxy_options(parser): + group = parser.add_argument_group("Proxy Options") + group.add_argument( + "-b", "--bind-address", + action="store", type=str, dest="addr", default='', + help="Address to bind proxy to (defaults to all interfaces)" + ) + group.add_argument( + "-I", "--ignore", + action="append", type=str, dest="ignore_hosts", default=[], + metavar="HOST", + help=""" + Ignore host and forward all traffic without processing it. In + transparent mode, it is recommended to use an IP address (range), + not the hostname. In regular mode, only SSL traffic is ignored and + the hostname should be used. The supplied value is interpreted as a + regular expression and matched on the ip or the hostname. Can be + passed multiple times. + """ + ) + group.add_argument( + "--tcp", + action="append", type=str, dest="tcp_hosts", default=[], + metavar="HOST", + help=""" + Generic TCP SSL proxy mode for all hosts that match the pattern. + Similar to --ignore, but SSL connections are intercepted. The + communication contents are printed to the event log in verbose mode. + """ + ) + group.add_argument( + "-n", "--no-server", + action="store_true", dest="no_server", + help="Don't start a proxy server." + ) + group.add_argument( + "-p", "--port", + action="store", type=int, dest="port", default=8080, + help="Proxy service port." + ) + http2 = group.add_mutually_exclusive_group() + # !!! + # Watch out: We raise a RuntimeError in mitmproxy.proxy.config if http2 is enabled, + # but the OpenSSL version does not have ALPN support (which is the default on Ubuntu 14.04). + # Do not simply set --http2 as enabled by default. + # !!! + http2.add_argument("--http2", action="store_true", dest="http2") + http2.add_argument("--no-http2", action="store_false", dest="http2", + help="Explicitly enable/disable experimental HTTP2 support. " + "Disabled by default. " + "Default value will change in a future version." + ) + rawtcp = group.add_mutually_exclusive_group() + rawtcp.add_argument("--raw-tcp", action="store_true", dest="rawtcp") + rawtcp.add_argument("--no-raw-tcp", action="store_false", dest="rawtcp", + help="Explicitly enable/disable experimental raw tcp support. " + "Disabled by default. " + "Default value will change in a future version." + ) + + +def proxy_ssl_options(parser): + # TODO: Agree to consistently either use "upstream" or "server". + group = parser.add_argument_group("SSL") + group.add_argument( + "--cert", + dest='certs', + default=[], + type=str, + metavar="SPEC", + action="append", + help='Add an SSL certificate. SPEC is of the form "[domain=]path". ' + 'The domain may include a wildcard, and is equal to "*" if not specified. ' + 'The file at path is a certificate in PEM format. If a private key is included ' + 'in the PEM, it is used, else the default key in the conf dir is used. ' + 'The PEM file should contain the full certificate chain, with the leaf certificate ' + 'as the first entry. Can be passed multiple times.') + group.add_argument( + "--ciphers-client", action="store", + type=str, dest="ciphers_client", default=config.DEFAULT_CLIENT_CIPHERS, + help="Set supported ciphers for client connections. (OpenSSL Syntax)" + ) + group.add_argument( + "--ciphers-server", action="store", + type=str, dest="ciphers_server", default=None, + help="Set supported ciphers for server connections. (OpenSSL Syntax)" + ) + group.add_argument( + "--client-certs", action="store", + type=str, dest="clientcerts", default=None, + help="Client certificate file or directory." + ) + group.add_argument( + "--no-upstream-cert", default=False, + action="store_true", dest="no_upstream_cert", + help="Don't connect to upstream server to look up certificate details." + ) + group.add_argument( + "--verify-upstream-cert", default=False, + action="store_true", dest="ssl_verify_upstream_cert", + help="Verify upstream server SSL/TLS certificates and fail if invalid " + "or not present." + ) + group.add_argument( + "--upstream-trusted-cadir", default=None, action="store", + dest="ssl_verify_upstream_trusted_cadir", + help="Path to a directory of trusted CA certificates for upstream " + "server verification prepared using the c_rehash tool." + ) + group.add_argument( + "--upstream-trusted-ca", default=None, action="store", + dest="ssl_verify_upstream_trusted_ca", + help="Path to a PEM formatted trusted CA certificate." + ) + group.add_argument( + "--ssl-version-client", dest="ssl_version_client", + default="secure", action="store", + choices=sslversion_choices.keys(), + help="Set supported SSL/TLS versions for client connections. " + "SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+." + ) + group.add_argument( + "--ssl-version-server", dest="ssl_version_server", + default="secure", action="store", + choices=sslversion_choices.keys(), + help="Set supported SSL/TLS versions for server connections. " + "SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+." + ) + + +def onboarding_app(parser): + group = parser.add_argument_group("Onboarding App") + group.add_argument( + "--noapp", + action="store_false", dest="app", default=True, + help="Disable the mitmproxy onboarding app." + ) + group.add_argument( + "--app-host", + action="store", dest="app_host", default=APP_HOST, metavar="host", + help=""" + Domain to serve the onboarding app from. For transparent mode, use + an IP when a DNS entry for the app domain is not present. Default: + %s + """ % APP_HOST + ) + group.add_argument( + "--app-port", + action="store", + dest="app_port", + default=APP_PORT, + type=int, + metavar="80", + help="Port to serve the onboarding app from." + ) + + +def client_replay(parser): + group = parser.add_argument_group("Client Replay") + group.add_argument( + "-c", "--client-replay", + action="append", dest="client_replay", default=None, metavar="PATH", + help="Replay client requests from a saved file." + ) + + +def server_replay(parser): + group = parser.add_argument_group("Server Replay") + group.add_argument( + "-S", "--server-replay", + action="append", dest="server_replay", default=None, metavar="PATH", + help="Replay server responses from a saved file." + ) + group.add_argument( + "-k", "--kill", + action="store_true", dest="kill", default=False, + help="Kill extra requests during replay." + ) + group.add_argument( + "--rheader", + action="append", dest="rheaders", type=str, + help="Request headers to be considered during replay. " + "Can be passed multiple times." + ) + group.add_argument( + "--norefresh", + action="store_true", dest="norefresh", default=False, + help=""" + Disable response refresh, which updates times in cookies and headers + for replayed responses. + """ + ) + group.add_argument( + "--no-pop", + action="store_true", dest="nopop", default=False, + help="Disable response pop from response flow. " + "This makes it possible to replay same response multiple times." + ) + payload = group.add_mutually_exclusive_group() + payload.add_argument( + "--replay-ignore-content", + action="store_true", dest="replay_ignore_content", default=False, + help=""" + Ignore request's content while searching for a saved flow to replay + """ + ) + payload.add_argument( + "--replay-ignore-payload-param", + action="append", dest="replay_ignore_payload_params", type=str, + help=""" + Request's payload parameters (application/x-www-form-urlencoded or multipart/form-data) to + be ignored while searching for a saved flow to replay. + Can be passed multiple times. + """ + ) + + group.add_argument( + "--replay-ignore-param", + action="append", dest="replay_ignore_params", type=str, + help=""" + Request's parameters to be ignored while searching for a saved flow + to replay. Can be passed multiple times. + """ + ) + group.add_argument( + "--replay-ignore-host", + action="store_true", + dest="replay_ignore_host", + default=False, + help="Ignore request's destination host while searching for a saved flow to replay") + + +def replacements(parser): + group = parser.add_argument_group( + "Replacements", + """ + Replacements are of the form "/pattern/regex/replacement", where + the separator can be any character. Please see the documentation + for more information. + """.strip() + ) + group.add_argument( + "--replace", + action="append", type=str, dest="replace", default=[], + metavar="PATTERN", + help="Replacement pattern." + ) + group.add_argument( + "--replace-from-file", + action="append", type=str, dest="replace_file", default=[], + metavar="PATH", + help=""" + Replacement pattern, where the replacement clause is a path to a + file. + """ + ) + + +def set_headers(parser): + group = parser.add_argument_group( + "Set Headers", + """ + Header specifications are of the form "/pattern/header/value", + where the separator can be any character. Please see the + documentation for more information. + """.strip() + ) + group.add_argument( + "--setheader", + action="append", type=str, dest="setheader", default=[], + metavar="PATTERN", + help="Header set pattern." + ) + + +def proxy_authentication(parser): + group = parser.add_argument_group( + "Proxy Authentication", + """ + Specify which users are allowed to access the proxy and the method + used for authenticating them. + """ + ).add_mutually_exclusive_group() + group.add_argument( + "--nonanonymous", + action="store_true", dest="auth_nonanonymous", + help="Allow access to any user long as a credentials are specified." + ) + + group.add_argument( + "--singleuser", + action="store", dest="auth_singleuser", type=str, + metavar="USER", + help=""" + Allows access to a a single user, specified in the form + username:password. + """ + ) + group.add_argument( + "--htpasswd", + action="store", dest="auth_htpasswd", type=str, + metavar="PATH", + help="Allow access to users specified in an Apache htpasswd file." + ) + + +def common_options(parser): + basic_options(parser) + proxy_modes(parser) + proxy_options(parser) + proxy_ssl_options(parser) + onboarding_app(parser) + client_replay(parser) + server_replay(parser) + replacements(parser) + set_headers(parser) + proxy_authentication(parser) + + +def mitmproxy(): + # Don't import mitmproxy.console for mitmdump, urwid is not available on all + # platforms. + from .console import palettes + + parser = configargparse.ArgumentParser( + usage="%(prog)s [options]", + args_for_setting_config_path=["--conf"], + default_config_files=[ + os.path.join(config.CA_DIR, "common.conf"), + os.path.join(config.CA_DIR, "mitmproxy.conf") + ], + add_config_file_help=True, + add_env_var_help=True + ) + common_options(parser) + parser.add_argument( + "--palette", type=str, default=palettes.DEFAULT, + action="store", dest="palette", + choices=sorted(palettes.palettes.keys()), + help="Select color palette: " + ", ".join(palettes.palettes.keys()) + ) + parser.add_argument( + "--palette-transparent", + action="store_true", dest="palette_transparent", default=False, + help="Set transparent background for palette." + ) + parser.add_argument( + "-e", "--eventlog", + action="store_true", dest="eventlog", + help="Show event log." + ) + parser.add_argument( + "-f", "--follow", + action="store_true", dest="follow", + help="Follow flow list." + ) + parser.add_argument( + "--no-mouse", + action="store_true", dest="no_mouse", + help="Disable mouse interaction." + ) + group = parser.add_argument_group( + "Filters", + "See help in mitmproxy for filter expression syntax." + ) + group.add_argument( + "-i", "--intercept", action="store", + type=str, dest="intercept", default=None, + help="Intercept filter expression." + ) + group.add_argument( + "-l", "--limit", action="store", + type=str, dest="limit", default=None, + help="Limit filter expression." + ) + return parser + + +def mitmdump(): + parser = configargparse.ArgumentParser( + usage="%(prog)s [options] [filter]", + args_for_setting_config_path=["--conf"], + default_config_files=[ + os.path.join(config.CA_DIR, "common.conf"), + os.path.join(config.CA_DIR, "mitmdump.conf") + ], + add_config_file_help=True, + add_env_var_help=True + ) + + common_options(parser) + parser.add_argument( + "--keepserving", + action="store_true", dest="keepserving", default=False, + help=""" + Continue serving after client playback or file read. We exit by + default. + """ + ) + parser.add_argument( + "-d", "--detail", + action="count", dest="flow_detail", default=1, + help="Increase flow detail display level. Can be passed multiple times." + ) + parser.add_argument('args', nargs="...") + return parser + + +def mitmweb(): + parser = configargparse.ArgumentParser( + usage="%(prog)s [options]", + args_for_setting_config_path=["--conf"], + default_config_files=[ + os.path.join(config.CA_DIR, "common.conf"), + os.path.join(config.CA_DIR, "mitmweb.conf") + ], + add_config_file_help=True, + add_env_var_help=True + ) + + group = parser.add_argument_group("Mitmweb") + group.add_argument( + "--wport", + action="store", type=int, dest="wport", default=8081, + metavar="PORT", + help="Mitmweb port." + ) + group.add_argument( + "--wiface", + action="store", dest="wiface", default="127.0.0.1", + metavar="IFACE", + help="Mitmweb interface." + ) + group.add_argument( + "--wdebug", + action="store_true", dest="wdebug", + help="Turn on mitmweb debugging" + ) + group.add_argument( + "--wsingleuser", + action="store", dest="wsingleuser", type=str, + metavar="USER", + help=""" + Allows access to a a single user, specified in the form + username:password. + """ + ) + group.add_argument( + "--whtpasswd", + action="store", dest="whtpasswd", type=str, + metavar="PATH", + help="Allow access to users specified in an Apache htpasswd file." + ) + + common_options(parser) + group = parser.add_argument_group( + "Filters", + "See help in mitmproxy for filter expression syntax." + ) + group.add_argument( + "-i", "--intercept", action="store", + type=str, dest="intercept", default=None, + help="Intercept filter expression." + ) + return parser diff --git a/mitmproxy/console/__init__.py b/mitmproxy/console/__init__.py new file mode 100644 index 00000000..e739ec61 --- /dev/null +++ b/mitmproxy/console/__init__.py @@ -0,0 +1,744 @@ +from __future__ import absolute_import + +import mailcap +import mimetypes +import tempfile +import os +import os.path +import shlex +import signal +import stat +import subprocess +import sys +import traceback +import urwid +import weakref + +from .. import controller, flow, script, contentviews +from . import flowlist, flowview, help, window, signals, options +from . import grideditor, palettes, statusbar, palettepicker + +EVENTLOG_SIZE = 500 + + +class ConsoleState(flow.State): + + def __init__(self): + flow.State.__init__(self) + self.focus = None + self.follow_focus = None + self.default_body_view = contentviews.get("Auto") + self.flowsettings = weakref.WeakKeyDictionary() + self.last_search = None + + def __setattr__(self, name, value): + self.__dict__[name] = value + signals.update_settings.send(self) + + def add_flow_setting(self, flow, key, value): + d = self.flowsettings.setdefault(flow, {}) + d[key] = value + + def get_flow_setting(self, flow, key, default=None): + d = self.flowsettings.get(flow, {}) + return d.get(key, default) + + def add_flow(self, f): + super(ConsoleState, self).add_flow(f) + if self.focus is None: + self.set_focus(0) + elif self.follow_focus: + self.set_focus(len(self.view) - 1) + self.set_flow_marked(f, False) + return f + + def update_flow(self, f): + super(ConsoleState, self).update_flow(f) + if self.focus is None: + self.set_focus(0) + return f + + def set_limit(self, limit): + ret = flow.State.set_limit(self, limit) + self.set_focus(self.focus) + return ret + + def get_focus(self): + if not self.view or self.focus is None: + return None, None + return self.view[self.focus], self.focus + + def set_focus(self, idx): + if self.view: + if idx >= len(self.view): + idx = len(self.view) - 1 + elif idx < 0: + idx = 0 + self.focus = idx + else: + self.focus = None + + def set_focus_flow(self, f): + self.set_focus(self.view.index(f)) + + def get_from_pos(self, pos): + if len(self.view) <= pos or pos < 0: + return None, None + return self.view[pos], pos + + def get_next(self, pos): + return self.get_from_pos(pos + 1) + + def get_prev(self, pos): + return self.get_from_pos(pos - 1) + + def delete_flow(self, f): + if f in self.view and self.view.index(f) <= self.focus: + self.focus -= 1 + if self.focus < 0: + self.focus = None + ret = flow.State.delete_flow(self, f) + self.set_focus(self.focus) + return ret + + def clear(self): + marked_flows = [] + for f in self.flows: + if self.flow_marked(f): + marked_flows.append(f) + + super(ConsoleState, self).clear() + + for f in marked_flows: + self.add_flow(f) + self.set_flow_marked(f, True) + + if len(self.flows.views) == 0: + self.focus = None + else: + self.focus = 0 + self.set_focus(self.focus) + + def flow_marked(self, flow): + return self.get_flow_setting(flow, "marked", False) + + def set_flow_marked(self, flow, marked): + self.add_flow_setting(flow, "marked", marked) + + +class Options(object): + attributes = [ + "app", + "app_domain", + "app_ip", + "anticache", + "anticomp", + "client_replay", + "eventlog", + "follow", + "keepserving", + "kill", + "intercept", + "limit", + "no_server", + "refresh_server_playback", + "rfile", + "scripts", + "showhost", + "replacements", + "rheaders", + "setheaders", + "server_replay", + "stickycookie", + "stickyauth", + "stream_large_bodies", + "verbosity", + "wfile", + "nopop", + "palette", + "palette_transparent", + "no_mouse" + ] + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + for i in self.attributes: + if not hasattr(self, i): + setattr(self, i, None) + + +class ConsoleMaster(flow.FlowMaster): + palette = [] + + def __init__(self, server, options): + flow.FlowMaster.__init__(self, server, ConsoleState()) + self.stream_path = None + self.options = options + + for i in options.replacements: + self.replacehooks.add(*i) + + for i in options.setheaders: + self.setheaders.add(*i) + + r = self.set_intercept(options.intercept) + if r: + print >> sys.stderr, "Intercept error:", r + sys.exit(1) + + if options.limit: + self.set_limit(options.limit) + + r = self.set_stickycookie(options.stickycookie) + if r: + print >> sys.stderr, "Sticky cookies error:", r + sys.exit(1) + + r = self.set_stickyauth(options.stickyauth) + if r: + print >> sys.stderr, "Sticky auth error:", r + sys.exit(1) + + self.set_stream_large_bodies(options.stream_large_bodies) + + self.refresh_server_playback = options.refresh_server_playback + self.anticache = options.anticache + self.anticomp = options.anticomp + self.killextra = options.kill + self.rheaders = options.rheaders + self.nopop = options.nopop + self.showhost = options.showhost + self.palette = options.palette + self.palette_transparent = options.palette_transparent + + self.eventlog = options.eventlog + self.eventlist = urwid.SimpleListWalker([]) + self.follow = options.follow + + if options.client_replay: + self.client_playback_path(options.client_replay) + + if options.server_replay: + self.server_playback_path(options.server_replay) + + if options.scripts: + for i in options.scripts: + err = self.load_script(i) + if err: + print >> sys.stderr, "Script load error:", err + sys.exit(1) + + if options.outfile: + err = self.start_stream_to_path( + options.outfile[0], + options.outfile[1] + ) + if err: + print >> sys.stderr, "Stream file error:", err + sys.exit(1) + + self.view_stack = [] + + if options.app: + self.start_app(self.options.app_host, self.options.app_port) + signals.call_in.connect(self.sig_call_in) + signals.pop_view_state.connect(self.sig_pop_view_state) + signals.push_view_state.connect(self.sig_push_view_state) + signals.sig_add_event.connect(self.sig_add_event) + + def __setattr__(self, name, value): + self.__dict__[name] = value + signals.update_settings.send(self) + + def load_script(self, command, use_reloader=True): + # We default to using the reloader in the console ui. + super(ConsoleMaster, self).load_script(command, use_reloader) + + def sig_add_event(self, sender, e, level): + needed = dict(error=0, info=1, debug=2).get(level, 1) + if self.options.verbosity < needed: + return + + if level == "error": + e = urwid.Text(("error", str(e))) + else: + e = urwid.Text(str(e)) + self.eventlist.append(e) + if len(self.eventlist) > EVENTLOG_SIZE: + self.eventlist.pop(0) + self.eventlist.set_focus(len(self.eventlist) - 1) + + def add_event(self, e, level): + signals.add_event(e, level) + + def sig_call_in(self, sender, seconds, callback, args=()): + def cb(*_): + return callback(*args) + self.loop.set_alarm_in(seconds, cb) + + def sig_pop_view_state(self, sender): + if len(self.view_stack) > 1: + self.view_stack.pop() + self.loop.widget = self.view_stack[-1] + else: + signals.status_prompt_onekey.send( + self, + prompt = "Quit", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.quit, + ) + + def sig_push_view_state(self, sender, window): + self.view_stack.append(window) + self.loop.widget = window + self.loop.draw_screen() + + def _run_script_method(self, method, s, f): + status, val = s.run(method, f) + if val: + if status: + signals.add_event("Method %s return: %s" % (method, val), "debug") + else: + signals.add_event( + "Method %s error: %s" % + (method, val[1]), "error") + + def run_script_once(self, command, f): + if not command: + return + signals.add_event("Running script on flow: %s" % command, "debug") + + try: + s = script.Script(command, script.ScriptContext(self)) + except script.ScriptException as v: + signals.status_message.send( + message = "Error loading script." + ) + signals.add_event("Error loading script:\n%s" % v.args[0], "error") + return + + if f.request: + self._run_script_method("request", s, f) + if f.response: + self._run_script_method("response", s, f) + if f.error: + self._run_script_method("error", s, f) + s.unload() + signals.flow_change.send(self, flow = f) + + def set_script(self, command): + if not command: + return + ret = self.load_script(command) + if ret: + signals.status_message.send(message=ret) + + def toggle_eventlog(self): + self.eventlog = not self.eventlog + signals.pop_view_state.send(self) + self.view_flowlist() + + def _readflows(self, path): + """ + Utitility function that reads a list of flows + or prints an error to the UI if that fails. + Returns + - None, if there was an error. + - a list of flows, otherwise. + """ + try: + return flow.read_flows_from_paths(path) + except flow.FlowReadError as e: + signals.status_message.send(message=e.strerror) + + def client_playback_path(self, path): + if not isinstance(path, list): + path = [path] + flows = self._readflows(path) + if flows: + self.start_client_playback(flows, False) + + def server_playback_path(self, path): + if not isinstance(path, list): + path = [path] + flows = self._readflows(path) + if flows: + self.start_server_playback( + flows, + self.killextra, self.rheaders, + False, self.nopop, + self.options.replay_ignore_params, + self.options.replay_ignore_content, + self.options.replay_ignore_payload_params, + self.options.replay_ignore_host + ) + + def spawn_editor(self, data): + fd, name = tempfile.mkstemp('', "mproxy") + os.write(fd, data) + os.close(fd) + c = os.environ.get("EDITOR") + # if no EDITOR is set, assume 'vi' + if not c: + c = "vi" + cmd = shlex.split(c) + cmd.append(name) + self.ui.stop() + try: + subprocess.call(cmd) + except: + signals.status_message.send( + message = "Can't start editor: %s" % " ".join(c) + ) + else: + data = open(name, "rb").read() + self.ui.start() + os.unlink(name) + return data + + def spawn_external_viewer(self, data, contenttype): + if contenttype: + contenttype = contenttype.split(";")[0] + ext = mimetypes.guess_extension(contenttype) or "" + else: + ext = "" + fd, name = tempfile.mkstemp(ext, "mproxy") + os.write(fd, data) + os.close(fd) + + # read-only to remind the user that this is a view function + os.chmod(name, stat.S_IREAD) + + cmd = None + shell = False + + if contenttype: + c = mailcap.getcaps() + cmd, _ = mailcap.findmatch(c, contenttype, filename=name) + if cmd: + shell = True + if not cmd: + # hm which one should get priority? + c = os.environ.get("PAGER") or os.environ.get("EDITOR") + if not c: + c = "less" + cmd = shlex.split(c) + cmd.append(name) + self.ui.stop() + try: + subprocess.call(cmd, shell=shell) + except: + signals.status_message.send( + message="Can't start external viewer: %s" % " ".join(c) + ) + self.ui.start() + os.unlink(name) + + def set_palette(self, name): + self.palette = name + self.ui.register_palette( + palettes.palettes[name].palette(self.palette_transparent) + ) + self.ui.clear() + + def ticker(self, *userdata): + changed = self.tick(self.masterq, timeout=0) + if changed: + self.loop.draw_screen() + signals.update_settings.send() + self.loop.set_alarm_in(0.01, self.ticker) + + def run(self): + self.ui = urwid.raw_display.Screen() + self.ui.set_terminal_properties(256) + self.set_palette(self.palette) + self.loop = urwid.MainLoop( + urwid.SolidFill("x"), + screen = self.ui, + handle_mouse = not self.options.no_mouse, + ) + + self.server.start_slave( + controller.Slave, + controller.Channel(self.masterq, self.should_exit) + ) + + if self.options.rfile: + ret = self.load_flows_path(self.options.rfile) + if ret and self.state.flow_count(): + signals.add_event( + "File truncated or corrupted. " + "Loaded as many flows as possible.", + "error" + ) + elif ret and not self.state.flow_count(): + self.shutdown() + print >> sys.stderr, "Could not load file:", ret + sys.exit(1) + + self.loop.set_alarm_in(0.01, self.ticker) + + # It's not clear why we need to handle this explicitly - without this, + # mitmproxy hangs on keyboard interrupt. Remove if we ever figure it + # out. + def exit(s, f): + raise urwid.ExitMainLoop + signal.signal(signal.SIGINT, exit) + + self.loop.set_alarm_in( + 0.0001, + lambda *args: self.view_flowlist() + ) + + try: + self.loop.run() + except Exception: + self.loop.stop() + sys.stdout.flush() + print >> sys.stderr, traceback.format_exc() + print >> sys.stderr, "mitmproxy has crashed!" + print >> sys.stderr, "Please lodge a bug report at:" + print >> sys.stderr, "\thttps://github.com/mitmproxy/mitmproxy" + print >> sys.stderr, "Shutting down..." + sys.stderr.flush() + self.shutdown() + + def view_help(self, helpctx): + signals.push_view_state.send( + self, + window = window.Window( + self, + help.HelpView(helpctx), + None, + statusbar.StatusBar(self, help.footer), + None + ) + ) + + def view_options(self): + for i in self.view_stack: + if isinstance(i["body"], options.Options): + return + signals.push_view_state.send( + self, + window = window.Window( + self, + options.Options(self), + None, + statusbar.StatusBar(self, options.footer), + options.help_context, + ) + ) + + def view_palette_picker(self): + signals.push_view_state.send( + self, + window = window.Window( + self, + palettepicker.PalettePicker(self), + None, + statusbar.StatusBar(self, palettepicker.footer), + palettepicker.help_context, + ) + ) + + def view_grideditor(self, ge): + signals.push_view_state.send( + self, + window = window.Window( + self, + ge, + None, + statusbar.StatusBar(self, grideditor.FOOTER), + ge.make_help() + ) + ) + + def view_flowlist(self): + if self.ui.started: + self.ui.clear() + if self.state.follow_focus: + self.state.set_focus(self.state.flow_count()) + + if self.eventlog: + body = flowlist.BodyPile(self) + else: + body = flowlist.FlowListBox(self) + + if self.follow: + self.toggle_follow_flows() + + signals.push_view_state.send( + self, + window = window.Window( + self, + body, + None, + statusbar.StatusBar(self, flowlist.footer), + flowlist.help_context + ) + ) + + def view_flow(self, flow, tab_offset=0): + self.state.set_focus_flow(flow) + signals.push_view_state.send( + self, + window = window.Window( + self, + flowview.FlowView(self, self.state, flow, tab_offset), + flowview.FlowViewHeader(self, flow), + statusbar.StatusBar(self, flowview.footer), + flowview.help_context + ) + ) + + def _write_flows(self, path, flows): + if not path: + return + path = os.path.expanduser(path) + try: + f = file(path, "wb") + fw = flow.FlowWriter(f) + for i in flows: + fw.add(i) + f.close() + except IOError as v: + signals.status_message.send(message=v.strerror) + + def save_one_flow(self, path, flow): + return self._write_flows(path, [flow]) + + def save_flows(self, path): + return self._write_flows(path, self.state.view) + + def save_marked_flows(self, path): + marked_flows = [] + for f in self.state.view: + if self.state.flow_marked(f): + marked_flows.append(f) + return self._write_flows(path, marked_flows) + + def load_flows_callback(self, path): + if not path: + return + ret = self.load_flows_path(path) + return ret or "Flows loaded from %s" % path + + def load_flows_path(self, path): + reterr = None + try: + flow.FlowMaster.load_flows_file(self, path) + except flow.FlowReadError as v: + reterr = str(v) + signals.flowlist_change.send(self) + return reterr + + def accept_all(self): + self.state.accept_all(self) + + def set_limit(self, txt): + v = self.state.set_limit(txt) + signals.flowlist_change.send(self) + return v + + def set_intercept(self, txt): + return self.state.set_intercept(txt) + + def change_default_display_mode(self, t): + v = contentviews.get_by_shortcut(t) + self.state.default_body_view = v + self.refresh_focus() + + def edit_scripts(self, scripts): + commands = [x[0] for x in scripts] # remove outer array + if commands == [s.command for s in self.scripts]: + return + + self.unload_scripts() + for command in commands: + self.load_script(command) + signals.update_settings.send(self) + + def stop_client_playback_prompt(self, a): + if a != "n": + self.stop_client_playback() + + def stop_server_playback_prompt(self, a): + if a != "n": + self.stop_server_playback() + + def quit(self, a): + if a != "n": + raise urwid.ExitMainLoop + + def shutdown(self): + self.state.killall(self) + flow.FlowMaster.shutdown(self) + + def clear_flows(self): + self.state.clear() + signals.flowlist_change.send(self) + + def toggle_follow_flows(self): + # toggle flow follow + self.state.follow_focus = not self.state.follow_focus + # jump to most recent flow if follow is now on + if self.state.follow_focus: + self.state.set_focus(self.state.flow_count()) + signals.flowlist_change.send(self) + + def delete_flow(self, f): + self.state.delete_flow(f) + signals.flowlist_change.send(self) + + def refresh_focus(self): + if self.state.view: + signals.flow_change.send( + self, + flow = self.state.view[self.state.focus] + ) + + def process_flow(self, f): + if self.state.intercept and f.match( + self.state.intercept) and not f.request.is_replay: + f.intercept(self) + else: + # check if flow was intercepted within an inline script by flow.intercept() + if f.intercepted: + f.intercept(self) + else: + f.reply() + signals.flowlist_change.send(self) + signals.flow_change.send(self, flow = f) + + def clear_events(self): + self.eventlist[:] = [] + + # Handlers + def handle_error(self, f): + f = flow.FlowMaster.handle_error(self, f) + if f: + self.process_flow(f) + return f + + def handle_request(self, f): + f = flow.FlowMaster.handle_request(self, f) + if f: + self.process_flow(f) + return f + + def handle_response(self, f): + f = flow.FlowMaster.handle_response(self, f) + if f: + self.process_flow(f) + return f + + def handle_script_change(self, script): + if super(ConsoleMaster, self).handle_script_change(script): + signals.status_message.send(message='"{}" reloaded.'.format(script.filename)) + else: + signals.status_message.send(message='Error reloading "{}".'.format(script.filename)) diff --git a/mitmproxy/console/common.py b/mitmproxy/console/common.py new file mode 100644 index 00000000..c29ffddc --- /dev/null +++ b/mitmproxy/console/common.py @@ -0,0 +1,444 @@ +from __future__ import absolute_import + +import urwid +import urwid.util +import os + +from netlib.http import CONTENT_MISSING +import netlib.utils + +from .. import utils +from .. import flow_export +from ..models import decoded +from . import signals + + +try: + import pyperclip +except: + pyperclip = False + + +VIEW_FLOW_REQUEST = 0 +VIEW_FLOW_RESPONSE = 1 + +METHOD_OPTIONS = [ + ("get", "g"), + ("post", "p"), + ("put", "u"), + ("head", "h"), + ("trace", "t"), + ("delete", "d"), + ("options", "o"), + ("edit raw", "e"), +] + + +def is_keypress(k): + """ + Is this input event a keypress? + """ + if isinstance(k, basestring): + return True + + +def highlight_key(str, key, textattr="text", keyattr="key"): + l = [] + parts = str.split(key, 1) + if parts[0]: + l.append((textattr, parts[0])) + l.append((keyattr, key)) + if parts[1]: + l.append((textattr, parts[1])) + return l + + +KEY_MAX = 30 + + +def format_keyvals(lst, key="key", val="text", indent=0): + """ + Format a list of (key, value) tuples. + + If key is None, it's treated specially: + - We assume a sub-value, and add an extra indent. + - The value is treated as a pre-formatted list of directives. + """ + ret = [] + if lst: + maxk = min(max(len(i[0]) for i in lst if i and i[0]), KEY_MAX) + for i, kv in enumerate(lst): + if kv is None: + ret.append(urwid.Text("")) + else: + if isinstance(kv[1], urwid.Widget): + v = kv[1] + elif kv[1] is None: + v = urwid.Text("") + else: + v = urwid.Text([(val, kv[1])]) + ret.append( + urwid.Columns( + [ + ("fixed", indent, urwid.Text("")), + ( + "fixed", + maxk, + urwid.Text([(key, kv[0] or "")]) + ), + v + ], + dividechars = 2 + ) + ) + return ret + + +def shortcuts(k): + if k == " ": + k = "page down" + elif k == "ctrl f": + k = "page down" + elif k == "ctrl b": + k = "page up" + elif k == "j": + k = "down" + elif k == "k": + k = "up" + return k + + +def fcol(s, attr): + s = unicode(s) + return ( + "fixed", + len(s), + urwid.Text( + [ + (attr, s) + ] + ) + ) + +if urwid.util.detected_encoding: + SYMBOL_REPLAY = u"\u21ba" + SYMBOL_RETURN = u"\u2190" + SYMBOL_MARK = u"\u25cf" +else: + SYMBOL_REPLAY = u"[r]" + SYMBOL_RETURN = u"<-" + SYMBOL_MARK = "[m]" + + +def raw_format_flow(f, focus, extended): + f = dict(f) + pile = [] + req = [] + if extended: + req.append( + fcol( + utils.format_timestamp(f["req_timestamp"]), + "highlight" + ) + ) + else: + req.append(fcol(">>" if focus else " ", "focus")) + + if f["marked"]: + req.append(fcol(SYMBOL_MARK, "mark")) + + if f["req_is_replay"]: + req.append(fcol(SYMBOL_REPLAY, "replay")) + req.append(fcol(f["req_method"], "method")) + + preamble = sum(i[1] for i in req) + len(req) - 1 + + if f["intercepted"] and not f["acked"]: + uc = "intercept" + elif f["resp_code"] or f["err_msg"]: + uc = "text" + else: + uc = "title" + + url = f["req_url"] + if f["req_http_version"] not in ("HTTP/1.0", "HTTP/1.1"): + url += " " + f["req_http_version"] + req.append( + urwid.Text([(uc, url)]) + ) + + pile.append(urwid.Columns(req, dividechars=1)) + + resp = [] + resp.append( + ("fixed", preamble, urwid.Text("")) + ) + + if f["resp_code"]: + codes = { + 2: "code_200", + 3: "code_300", + 4: "code_400", + 5: "code_500", + } + ccol = codes.get(f["resp_code"] / 100, "code_other") + resp.append(fcol(SYMBOL_RETURN, ccol)) + if f["resp_is_replay"]: + resp.append(fcol(SYMBOL_REPLAY, "replay")) + resp.append(fcol(f["resp_code"], ccol)) + if f["intercepted"] and f["resp_code"] and not f["acked"]: + rc = "intercept" + else: + rc = "text" + + if f["resp_ctype"]: + resp.append(fcol(f["resp_ctype"], rc)) + resp.append(fcol(f["resp_clen"], rc)) + resp.append(fcol(f["roundtrip"], rc)) + + elif f["err_msg"]: + resp.append(fcol(SYMBOL_RETURN, "error")) + resp.append( + urwid.Text([ + ( + "error", + f["err_msg"] + ) + ]) + ) + pile.append(urwid.Columns(resp, dividechars=1)) + return urwid.Pile(pile) + + +# Save file to disk +def save_data(path, data): + if not path: + return + try: + with file(path, "wb") as f: + f.write(data) + except IOError as v: + signals.status_message.send(message=v.strerror) + + +def ask_save_overwrite(path, data): + if not path: + return + path = os.path.expanduser(path) + if os.path.exists(path): + def save_overwrite(k): + if k == "y": + save_data(path, data) + + signals.status_prompt_onekey.send( + prompt = "'" + path + "' already exists. Overwrite?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = save_overwrite + ) + else: + save_data(path, data) + + +def ask_save_path(prompt, data): + signals.status_prompt_path.send( + prompt = prompt, + callback = ask_save_overwrite, + args = (data, ) + ) + + +def copy_flow_format_data(part, scope, flow): + if part == "u": + data = flow.request.url + else: + data = "" + if scope in ("q", "a"): + if flow.request.content is None or flow.request.content == CONTENT_MISSING: + return None, "Request content is missing" + with decoded(flow.request): + if part == "h": + data += netlib.http.http1.assemble_request(flow.request) + elif part == "c": + data += flow.request.content + else: + raise ValueError("Unknown part: {}".format(part)) + if scope == "a" and flow.request.content and flow.response: + # Add padding between request and response + data += "\r\n" * 2 + if scope in ("s", "a") and flow.response: + if flow.response.content is None or flow.response.content == CONTENT_MISSING: + return None, "Response content is missing" + with decoded(flow.response): + if part == "h": + data += netlib.http.http1.assemble_response(flow.response) + elif part == "c": + data += flow.response.content + else: + raise ValueError("Unknown part: {}".format(part)) + return data, False + + +def export_prompt(k, flow): + exporters = { + "c": flow_export.curl_command, + "p": flow_export.python_code, + "r": flow_export.raw_request, + } + if k in exporters: + copy_to_clipboard_or_prompt(exporters[k](flow)) + + +def copy_to_clipboard_or_prompt(data): + # pyperclip calls encode('utf-8') on data to be copied without checking. + # if data are already encoded that way UnicodeDecodeError is thrown. + toclip = "" + try: + toclip = data.decode('utf-8') + except (UnicodeDecodeError): + toclip = data + + try: + pyperclip.copy(toclip) + except (RuntimeError, UnicodeDecodeError, AttributeError): + def save(k): + if k == "y": + ask_save_path("Save data", data) + signals.status_prompt_onekey.send( + prompt = "Cannot copy data to clipboard. Save as file?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = save + ) + + +def copy_flow(part, scope, flow, master, state): + """ + part: _c_ontent, _h_eaders+content, _u_rl + scope: _a_ll, re_q_uest, re_s_ponse + """ + data, err = copy_flow_format_data(part, scope, flow) + + if err: + signals.status_message.send(message=err) + return + + if not data: + if scope == "q": + signals.status_message.send(message="No request content to copy.") + elif scope == "s": + signals.status_message.send(message="No response content to copy.") + else: + signals.status_message.send(message="No contents to copy.") + return + + copy_to_clipboard_or_prompt(data) + + +def ask_copy_part(scope, flow, master, state): + choices = [ + ("content", "c"), + ("headers+content", "h") + ] + if scope != "s": + choices.append(("url", "u")) + + signals.status_prompt_onekey.send( + prompt = "Copy", + keys = choices, + callback = copy_flow, + args = (scope, flow, master, state) + ) + + +def ask_save_body(part, master, state, flow): + """ + Save either the request or the response body to disk. part can either be + "q" (request), "s" (response) or None (ask user if necessary). + """ + + request_has_content = flow.request and flow.request.content + response_has_content = flow.response and flow.response.content + + if part is None: + # We first need to determine whether we want to save the request or the + # response content. + if request_has_content and response_has_content: + signals.status_prompt_onekey.send( + prompt = "Save", + keys = ( + ("request", "q"), + ("response", "s"), + ), + callback = ask_save_body, + args = (master, state, flow) + ) + elif response_has_content: + ask_save_body("s", master, state, flow) + else: + ask_save_body("q", master, state, flow) + + elif part == "q" and request_has_content: + ask_save_path( + "Save request content", + flow.request.get_decoded_content() + ) + elif part == "s" and response_has_content: + ask_save_path( + "Save response content", + flow.response.get_decoded_content() + ) + else: + signals.status_message.send(message="No content to save.") + + +flowcache = utils.LRUCache(800) + + +def format_flow(f, focus, extended=False, hostheader=False, marked=False): + d = dict( + intercepted = f.intercepted, + acked = f.reply.acked, + + req_timestamp = f.request.timestamp_start, + req_is_replay = f.request.is_replay, + req_method = f.request.method, + req_url = f.request.pretty_url if hostheader else f.request.url, + req_http_version = f.request.http_version, + + err_msg = f.error.msg if f.error else None, + resp_code = f.response.status_code if f.response else None, + + marked = marked, + ) + if f.response: + if f.response.content: + contentdesc = netlib.utils.pretty_size(len(f.response.content)) + elif f.response.content == CONTENT_MISSING: + contentdesc = "[content missing]" + else: + contentdesc = "[no content]" + duration = 0 + if f.response.timestamp_end and f.request.timestamp_start: + duration = f.response.timestamp_end - f.request.timestamp_start + roundtrip = utils.pretty_duration(duration) + + d.update(dict( + resp_code = f.response.status_code, + resp_is_replay = f.response.is_replay, + resp_clen = contentdesc, + roundtrip = roundtrip, + )) + t = f.response.headers.get("content-type") + if t: + d["resp_ctype"] = t.split(";")[0] + else: + d["resp_ctype"] = "" + return flowcache.get( + raw_format_flow, + tuple(sorted(d.items())), focus, extended + ) diff --git a/mitmproxy/console/flowdetailview.py b/mitmproxy/console/flowdetailview.py new file mode 100644 index 00000000..f4b4262e --- /dev/null +++ b/mitmproxy/console/flowdetailview.py @@ -0,0 +1,153 @@ +from __future__ import absolute_import +import urwid +from . import common, searchable +from .. import utils + + +def maybe_timestamp(base, attr): + if base and getattr(base, attr): + return utils.format_timestamp_with_milli(getattr(base, attr)) + else: + return "active" + + +def flowdetails(state, flow): + text = [] + + cc = flow.client_conn + sc = flow.server_conn + req = flow.request + resp = flow.response + + if sc is not None: + text.append(urwid.Text([("head", "Server Connection:")])) + parts = [ + ["Address", "%s:%s" % sc.address()], + ] + + text.extend( + common.format_keyvals(parts, key="key", val="text", indent=4) + ) + + c = sc.cert + if c: + text.append(urwid.Text([("head", "Server Certificate:")])) + parts = [ + ["Type", "%s, %s bits" % c.keyinfo], + ["SHA1 digest", c.digest("sha1")], + ["Valid to", str(c.notafter)], + ["Valid from", str(c.notbefore)], + ["Serial", str(c.serial)], + [ + "Subject", + urwid.BoxAdapter( + urwid.ListBox( + common.format_keyvals( + c.subject, + key="highlight", + val="text" + ) + ), + len(c.subject) + ) + ], + [ + "Issuer", + urwid.BoxAdapter( + urwid.ListBox( + common.format_keyvals( + c.issuer, key="highlight", val="text" + ) + ), + len(c.issuer) + ) + ] + ] + + if c.altnames: + parts.append( + [ + "Alt names", + ", ".join(c.altnames) + ] + ) + text.extend( + common.format_keyvals(parts, key="key", val="text", indent=4) + ) + + if cc is not None: + text.append(urwid.Text([("head", "Client Connection:")])) + + parts = [ + ["Address", "%s:%s" % cc.address()], + # ["Requests", "%s"%cc.requestcount], + ] + + text.extend( + common.format_keyvals(parts, key="key", val="text", indent=4) + ) + + parts = [] + + parts.append( + [ + "Client conn. established", + maybe_timestamp(cc, "timestamp_start") + ] + ) + parts.append( + [ + "Server conn. initiated", + maybe_timestamp(sc, "timestamp_start") + ] + ) + parts.append( + [ + "Server conn. TCP handshake", + maybe_timestamp(sc, "timestamp_tcp_setup") + ] + ) + if sc.ssl_established: + parts.append( + [ + "Server conn. SSL handshake", + maybe_timestamp(sc, "timestamp_ssl_setup") + ] + ) + parts.append( + [ + "Client conn. SSL handshake", + maybe_timestamp(cc, "timestamp_ssl_setup") + ] + ) + parts.append( + [ + "First request byte", + maybe_timestamp(req, "timestamp_start") + ] + ) + parts.append( + [ + "Request complete", + maybe_timestamp(req, "timestamp_end") + ] + ) + parts.append( + [ + "First response byte", + maybe_timestamp(resp, "timestamp_start") + ] + ) + parts.append( + [ + "Response complete", + maybe_timestamp(resp, "timestamp_end") + ] + ) + + # sort operations by timestamp + parts = sorted(parts, key=lambda p: p[1]) + + text.append(urwid.Text([("head", "Timing:")])) + text.extend(common.format_keyvals(parts, key="key", val="text", indent=4)) + return searchable.Searchable(state, text) diff --git a/mitmproxy/console/flowlist.py b/mitmproxy/console/flowlist.py new file mode 100644 index 00000000..c2201055 --- /dev/null +++ b/mitmproxy/console/flowlist.py @@ -0,0 +1,397 @@ +from __future__ import absolute_import +import urwid + +import netlib.utils + +from . import common, signals + + +def _mkhelp(): + text = [] + keys = [ + ("A", "accept all intercepted flows"), + ("a", "accept this intercepted flow"), + ("b", "save request/response body"), + ("C", "clear flow list or eventlog"), + ("d", "delete flow"), + ("D", "duplicate flow"), + ("E", "export"), + ("e", "toggle eventlog"), + ("F", "toggle follow flow list"), + ("l", "set limit filter pattern"), + ("L", "load saved flows"), + ("m", "toggle flow mark"), + ("n", "create a new request"), + ("P", "copy flow to clipboard"), + ("r", "replay request"), + ("U", "unmark all marked flows"), + ("V", "revert changes to request"), + ("w", "save flows "), + ("W", "stream flows to file"), + ("X", "kill and delete flow, even if it's mid-intercept"), + ("tab", "tab between eventlog and flow list"), + ("enter", "view flow"), + ("|", "run script on this flow"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + +footer = [ + ('heading_key', "?"), ":help ", +] + + +class EventListBox(urwid.ListBox): + + def __init__(self, master): + self.master = master + urwid.ListBox.__init__(self, master.eventlist) + + def keypress(self, size, key): + key = common.shortcuts(key) + if key == "C": + self.master.clear_events() + key = None + elif key == "G": + self.set_focus(len(self.master.eventlist) - 1) + elif key == "g": + self.set_focus(0) + return urwid.ListBox.keypress(self, size, key) + + +class BodyPile(urwid.Pile): + + def __init__(self, master): + h = urwid.Text("Event log") + h = urwid.Padding(h, align="left", width=("relative", 100)) + + self.inactive_header = urwid.AttrWrap(h, "heading_inactive") + self.active_header = urwid.AttrWrap(h, "heading") + + urwid.Pile.__init__( + self, + [ + FlowListBox(master), + urwid.Frame( + EventListBox(master), + header = self.inactive_header + ) + ] + ) + self.master = master + + def keypress(self, size, key): + if key == "tab": + self.focus_position = ( + self.focus_position + 1) % len(self.widget_list) + if self.focus_position == 1: + self.widget_list[1].header = self.active_header + else: + self.widget_list[1].header = self.inactive_header + key = None + elif key == "e": + self.master.toggle_eventlog() + key = None + + # This is essentially a copypasta from urwid.Pile's keypress handler. + # So much for "closed for modification, but open for extension". + item_rows = None + if len(size) == 2: + item_rows = self.get_item_rows(size, focus = True) + i = self.widget_list.index(self.focus_item) + tsize = self.get_item_size(size, i, True, item_rows) + return self.focus_item.keypress(tsize, key) + + +class ConnectionItem(urwid.WidgetWrap): + + def __init__(self, master, state, flow, focus): + self.master, self.state, self.flow = master, state, flow + self.f = focus + w = self.get_text() + urwid.WidgetWrap.__init__(self, w) + + def get_text(self): + return common.format_flow( + self.flow, + self.f, + hostheader = self.master.showhost, + marked=self.state.flow_marked(self.flow) + ) + + def selectable(self): + return True + + def save_flows_prompt(self, k): + if k == "a": + signals.status_prompt_path.send( + prompt = "Save all flows to", + callback = self.master.save_flows + ) + elif k == "m": + signals.status_prompt_path.send( + prompt = "Save marked flows to", + callback = self.master.save_marked_flows + ) + else: + signals.status_prompt_path.send( + prompt = "Save this flow to", + callback = self.master.save_one_flow, + args = (self.flow,) + ) + + def stop_server_playback_prompt(self, a): + if a != "n": + self.master.stop_server_playback() + + def server_replay_prompt(self, k): + if k == "a": + self.master.start_server_playback( + [i.copy() for i in self.master.state.view], + self.master.killextra, self.master.rheaders, + False, self.master.nopop, + self.master.options.replay_ignore_params, + self.master.options.replay_ignore_content, + self.master.options.replay_ignore_payload_params, + self.master.options.replay_ignore_host + ) + elif k == "t": + self.master.start_server_playback( + [self.flow.copy()], + self.master.killextra, self.master.rheaders, + False, self.master.nopop, + self.master.options.replay_ignore_params, + self.master.options.replay_ignore_content, + self.master.options.replay_ignore_payload_params, + self.master.options.replay_ignore_host + ) + else: + signals.status_prompt_path.send( + prompt = "Server replay path", + callback = self.master.server_playback_path + ) + + def mouse_event(self, size, event, button, col, row, focus): + if event == "mouse press" and button == 1: + if self.flow.request: + self.master.view_flow(self.flow) + return True + + def keypress(self, xxx_todo_changeme, key): + (maxcol,) = xxx_todo_changeme + key = common.shortcuts(key) + if key == "a": + self.flow.accept_intercept(self.master) + signals.flowlist_change.send(self) + elif key == "d": + self.flow.kill(self.master) + self.state.delete_flow(self.flow) + signals.flowlist_change.send(self) + elif key == "D": + f = self.master.duplicate_flow(self.flow) + self.master.view_flow(f) + elif key == "m": + if self.state.flow_marked(self.flow): + self.state.set_flow_marked(self.flow, False) + else: + self.state.set_flow_marked(self.flow, True) + signals.flowlist_change.send(self) + elif key == "r": + r = self.master.replay_request(self.flow) + if r: + signals.status_message.send(message=r) + signals.flowlist_change.send(self) + elif key == "S": + if not self.master.server_playback: + signals.status_prompt_onekey.send( + prompt = "Server Replay", + keys = ( + ("all flows", "a"), + ("this flow", "t"), + ("file", "f"), + ), + callback = self.server_replay_prompt, + ) + else: + signals.status_prompt_onekey.send( + prompt = "Stop current server replay?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.stop_server_playback_prompt, + ) + elif key == "U": + for f in self.state.flows: + self.state.set_flow_marked(f, False) + signals.flowlist_change.send(self) + elif key == "V": + if not self.flow.modified(): + signals.status_message.send(message="Flow not modified.") + return + self.state.revert(self.flow) + signals.flowlist_change.send(self) + signals.status_message.send(message="Reverted.") + elif key == "w": + signals.status_prompt_onekey.send( + self, + prompt = "Save", + keys = ( + ("all flows", "a"), + ("this flow", "t"), + ("marked flows", "m"), + ), + callback = self.save_flows_prompt, + ) + elif key == "X": + self.flow.kill(self.master) + elif key == "enter": + if self.flow.request: + self.master.view_flow(self.flow) + elif key == "|": + signals.status_prompt_path.send( + prompt = "Send flow to script", + callback = self.master.run_script_once, + args = (self.flow,) + ) + elif key == "P": + common.ask_copy_part("a", self.flow, self.master, self.state) + elif key == "E": + signals.status_prompt_onekey.send( + self, + prompt = "Export", + keys = ( + ("as curl command", "c"), + ("as python code", "p"), + ("as raw request", "r"), + ), + callback = common.export_prompt, + args = (self.flow,) + ) + elif key == "b": + common.ask_save_body(None, self.master, self.state, self.flow) + else: + return key + + +class FlowListWalker(urwid.ListWalker): + + def __init__(self, master, state): + self.master, self.state = master, state + signals.flowlist_change.connect(self.sig_flowlist_change) + + def sig_flowlist_change(self, sender): + self._modified() + + def get_focus(self): + f, i = self.state.get_focus() + f = ConnectionItem(self.master, self.state, f, True) if f else None + return f, i + + def set_focus(self, focus): + ret = self.state.set_focus(focus) + return ret + + def get_next(self, pos): + f, i = self.state.get_next(pos) + f = ConnectionItem(self.master, self.state, f, False) if f else None + return f, i + + def get_prev(self, pos): + f, i = self.state.get_prev(pos) + f = ConnectionItem(self.master, self.state, f, False) if f else None + return f, i + + +class FlowListBox(urwid.ListBox): + + def __init__(self, master): + self.master = master + urwid.ListBox.__init__( + self, + FlowListWalker(master, master.state) + ) + + def get_method_raw(self, k): + if k: + self.get_url(k) + + def get_method(self, k): + if k == "e": + signals.status_prompt.send( + self, + prompt = "Method", + text = "", + callback = self.get_method_raw + ) + else: + method = "" + for i in common.METHOD_OPTIONS: + if i[1] == k: + method = i[0].upper() + self.get_url(method) + + def get_url(self, method): + signals.status_prompt.send( + prompt = "URL", + text = "http://www.example.com/", + callback = self.new_request, + args = (method,) + ) + + def new_request(self, url, method): + parts = netlib.utils.parse_url(str(url)) + if not parts: + signals.status_message.send(message="Invalid Url") + return + scheme, host, port, path = parts + f = self.master.create_request(method, scheme, host, port, path) + self.master.view_flow(f) + + def keypress(self, size, key): + key = common.shortcuts(key) + if key == "A": + self.master.accept_all() + signals.flowlist_change.send(self) + elif key == "C": + self.master.clear_flows() + elif key == "e": + self.master.toggle_eventlog() + elif key == "g": + self.master.state.set_focus(0) + signals.flowlist_change.send(self) + elif key == "G": + self.master.state.set_focus(self.master.state.flow_count()) + signals.flowlist_change.send(self) + elif key == "l": + signals.status_prompt.send( + prompt = "Limit", + text = self.master.state.limit_txt, + callback = self.master.set_limit + ) + elif key == "L": + signals.status_prompt_path.send( + self, + prompt = "Load flows", + callback = self.master.load_flows_callback + ) + elif key == "n": + signals.status_prompt_onekey.send( + prompt = "Method", + keys = common.METHOD_OPTIONS, + callback = self.get_method + ) + elif key == "F": + self.master.toggle_follow_flows() + elif key == "W": + if self.master.stream: + self.master.stop_stream() + else: + signals.status_prompt_path.send( + self, + prompt = "Stream flows to", + callback = self.master.start_stream_to_path + ) + else: + return urwid.ListBox.keypress(self, size, key) diff --git a/mitmproxy/console/flowview.py b/mitmproxy/console/flowview.py new file mode 100644 index 00000000..f74ab140 --- /dev/null +++ b/mitmproxy/console/flowview.py @@ -0,0 +1,714 @@ +from __future__ import absolute_import, division +import os +import traceback +import sys + +import math +import urwid + +from netlib import odict +from netlib.http import CONTENT_MISSING, Headers +from . import common, grideditor, signals, searchable, tabs +from . import flowdetailview +from .. import utils, controller, contentviews +from ..models import HTTPRequest, HTTPResponse, decoded +from ..exceptions import ContentViewException + + +class SearchError(Exception): + pass + + +def _mkhelp(): + text = [] + keys = [ + ("A", "accept all intercepted flows"), + ("a", "accept this intercepted flow"), + ("b", "save request/response body"), + ("D", "duplicate flow"), + ("d", "delete flow"), + ("E", "export"), + ("e", "edit request/response"), + ("f", "load full body data"), + ("m", "change body display mode for this entity"), + (None, + common.highlight_key("automatic", "a") + + [("text", ": automatic detection")] + ), + (None, + common.highlight_key("hex", "e") + + [("text", ": Hex")] + ), + (None, + common.highlight_key("html", "h") + + [("text", ": HTML")] + ), + (None, + common.highlight_key("image", "i") + + [("text", ": Image")] + ), + (None, + common.highlight_key("javascript", "j") + + [("text", ": JavaScript")] + ), + (None, + common.highlight_key("json", "s") + + [("text", ": JSON")] + ), + (None, + common.highlight_key("urlencoded", "u") + + [("text", ": URL-encoded data")] + ), + (None, + common.highlight_key("raw", "r") + + [("text", ": raw data")] + ), + (None, + common.highlight_key("xml", "x") + + [("text", ": XML")] + ), + ("M", "change default body display mode"), + ("p", "previous flow"), + ("P", "copy request/response (content/headers) to clipboard"), + ("r", "replay request"), + ("V", "revert changes to request"), + ("v", "view body in external viewer"), + ("w", "save all flows matching current limit"), + ("W", "save this flow"), + ("x", "delete body"), + ("z", "encode/decode a request/response"), + ("tab", "next tab"), + ("h, l", "previous tab, next tab"), + ("space", "next flow"), + ("|", "run script on this flow"), + ("/", "search (case sensitive)"), + ("n", "repeat search forward"), + ("N", "repeat search backwards"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + +footer = [ + ('heading_key', "?"), ":help ", + ('heading_key', "q"), ":back ", +] + + +class FlowViewHeader(urwid.WidgetWrap): + + def __init__(self, master, f): + self.master, self.flow = master, f + self._w = common.format_flow( + f, + False, + extended=True, + hostheader=self.master.showhost + ) + signals.flow_change.connect(self.sig_flow_change) + + def sig_flow_change(self, sender, flow): + if flow == self.flow: + self._w = common.format_flow( + flow, + False, + extended=True, + hostheader=self.master.showhost + ) + + +cache = utils.LRUCache(200) + +TAB_REQ = 0 +TAB_RESP = 1 + + +class FlowView(tabs.Tabs): + highlight_color = "focusfield" + + def __init__(self, master, state, flow, tab_offset): + self.master, self.state, self.flow = master, state, flow + tabs.Tabs.__init__(self, + [ + (self.tab_request, self.view_request), + (self.tab_response, self.view_response), + (self.tab_details, self.view_details), + ], + tab_offset + ) + self.show() + self.last_displayed_body = None + signals.flow_change.connect(self.sig_flow_change) + + def tab_request(self): + if self.flow.intercepted and not self.flow.reply.acked and not self.flow.response: + return "Request intercepted" + else: + return "Request" + + def tab_response(self): + if self.flow.intercepted and not self.flow.reply.acked and self.flow.response: + return "Response intercepted" + else: + return "Response" + + def tab_details(self): + return "Detail" + + def view_request(self): + return self.conn_text(self.flow.request) + + def view_response(self): + return self.conn_text(self.flow.response) + + def view_details(self): + return flowdetailview.flowdetails(self.state, self.flow) + + def sig_flow_change(self, sender, flow): + if flow == self.flow: + self.show() + + def content_view(self, viewmode, message): + if message.content == CONTENT_MISSING: + msg, body = "", [urwid.Text([("error", "[content missing]")])] + return msg, body + else: + full = self.state.get_flow_setting( + self.flow, + (self.tab_offset, "fullcontents"), + False + ) + if full: + limit = sys.maxsize + else: + limit = contentviews.VIEW_CUTOFF + return cache.get( + self._get_content_view, + viewmode, + message, + limit, + (bytes(message.headers), message.content) # Cache invalidation + ) + + def _get_content_view(self, viewmode, message, max_lines, _): + + try: + query = None + if isinstance(message, HTTPRequest): + query = message.query + description, lines = contentviews.get_content_view( + viewmode, message.content, headers=message.headers, query=query + ) + except ContentViewException: + s = "Content viewer failed: \n" + traceback.format_exc() + signals.add_event(s, "error") + description, lines = contentviews.get_content_view( + contentviews.get("Raw"), message.content, headers=message.headers + ) + description = description.replace("Raw", "Couldn't parse: falling back to Raw") + + # Give hint that you have to tab for the response. + if description == "No content" and isinstance(message, HTTPRequest): + description = "No request content (press tab to view response)" + + # If the users has a wide terminal, he gets fewer lines; this should not be an issue. + chars_per_line = 80 + max_chars = max_lines * chars_per_line + total_chars = 0 + text_objects = [] + for line in lines: + txt = [] + for (style, text) in line: + if total_chars + len(text) > max_chars: + text = text[:max_chars - total_chars] + txt.append((style, text)) + total_chars += len(text) + if total_chars == max_chars: + break + + # round up to the next line. + total_chars = int(math.ceil(total_chars / chars_per_line) * chars_per_line) + + text_objects.append(urwid.Text(txt)) + if total_chars == max_chars: + text_objects.append(urwid.Text([ + ("highlight", "Stopped displaying data after %d lines. Press " % max_lines), + ("key", "f"), + ("highlight", " to load all data.") + ])) + break + + return description, text_objects + + def viewmode_get(self): + override = self.state.get_flow_setting( + self.flow, + (self.tab_offset, "prettyview") + ) + return self.state.default_body_view if override is None else override + + def conn_text(self, conn): + if conn: + txt = common.format_keyvals( + [(h + ":", v) for (h, v) in conn.headers.fields], + key = "header", + val = "text" + ) + viewmode = self.viewmode_get() + msg, body = self.content_view(viewmode, conn) + + cols = [ + urwid.Text( + [ + ("heading", msg), + ] + ), + urwid.Text( + [ + " ", + ('heading', "["), + ('heading_key', "m"), + ('heading', (":%s]" % viewmode.name)), + ], + align="right" + ) + ] + title = urwid.AttrWrap(urwid.Columns(cols), "heading") + + txt.append(title) + txt.extend(body) + else: + txt = [ + urwid.Text(""), + urwid.Text( + [ + ("highlight", "No response. Press "), + ("key", "e"), + ("highlight", " and edit any aspect to add one."), + ] + ) + ] + return searchable.Searchable(self.state, txt) + + def set_method_raw(self, m): + if m: + self.flow.request.method = m + signals.flow_change.send(self, flow = self.flow) + + def edit_method(self, m): + if m == "e": + signals.status_prompt.send( + prompt = "Method", + text = self.flow.request.method, + callback = self.set_method_raw + ) + else: + for i in common.METHOD_OPTIONS: + if i[1] == m: + self.flow.request.method = i[0].upper() + signals.flow_change.send(self, flow = self.flow) + + def set_url(self, url): + request = self.flow.request + try: + request.url = str(url) + except ValueError: + return "Invalid URL." + signals.flow_change.send(self, flow = self.flow) + + def set_resp_code(self, code): + response = self.flow.response + try: + response.status_code = int(code) + except ValueError: + return None + import BaseHTTPServer + if int(code) in BaseHTTPServer.BaseHTTPRequestHandler.responses: + response.msg = BaseHTTPServer.BaseHTTPRequestHandler.responses[ + int(code)][0] + signals.flow_change.send(self, flow = self.flow) + + def set_resp_msg(self, msg): + response = self.flow.response + response.msg = msg + signals.flow_change.send(self, flow = self.flow) + + def set_headers(self, fields, conn): + conn.headers = Headers(fields) + signals.flow_change.send(self, flow = self.flow) + + def set_query(self, lst, conn): + conn.set_query(odict.ODict(lst)) + signals.flow_change.send(self, flow = self.flow) + + def set_path_components(self, lst, conn): + conn.set_path_components(lst) + signals.flow_change.send(self, flow = self.flow) + + def set_form(self, lst, conn): + conn.set_form_urlencoded(odict.ODict(lst)) + signals.flow_change.send(self, flow = self.flow) + + def edit_form(self, conn): + self.master.view_grideditor( + grideditor.URLEncodedFormEditor( + self.master, + conn.get_form_urlencoded().lst, + self.set_form, + conn + ) + ) + + def edit_form_confirm(self, key, conn): + if key == "y": + self.edit_form(conn) + + def set_cookies(self, lst, conn): + od = odict.ODict(lst) + conn.set_cookies(od) + signals.flow_change.send(self, flow = self.flow) + + def set_setcookies(self, data, conn): + conn.set_cookies(data) + signals.flow_change.send(self, flow = self.flow) + + def edit(self, part): + if self.tab_offset == TAB_REQ: + message = self.flow.request + else: + if not self.flow.response: + self.flow.response = HTTPResponse( + self.flow.request.http_version, + 200, "OK", Headers(), "" + ) + self.flow.response.reply = controller.DummyReply() + message = self.flow.response + + self.flow.backup() + if message == self.flow.request and part == "c": + self.master.view_grideditor( + grideditor.CookieEditor( + self.master, + message.get_cookies().lst, + self.set_cookies, + message + ) + ) + if message == self.flow.response and part == "c": + self.master.view_grideditor( + grideditor.SetCookieEditor( + self.master, + message.get_cookies(), + self.set_setcookies, + message + ) + ) + if part == "r": + with decoded(message): + # Fix an issue caused by some editors when editing a + # request/response body. Many editors make it hard to save a + # file without a terminating newline on the last line. When + # editing message bodies, this can cause problems. For now, I + # just strip the newlines off the end of the body when we return + # from an editor. + c = self.master.spawn_editor(message.content or "") + message.content = c.rstrip("\n") + elif part == "f": + if not message.get_form_urlencoded() and message.content: + signals.status_prompt_onekey.send( + prompt = "Existing body is not a URL-encoded form. Clear and edit?", + keys = [ + ("yes", "y"), + ("no", "n"), + ], + callback = self.edit_form_confirm, + args = (message,) + ) + else: + self.edit_form(message) + elif part == "h": + self.master.view_grideditor( + grideditor.HeaderEditor( + self.master, + message.headers.fields, + self.set_headers, + message + ) + ) + elif part == "p": + p = message.get_path_components() + self.master.view_grideditor( + grideditor.PathEditor( + self.master, + p, + self.set_path_components, + message + ) + ) + elif part == "q": + self.master.view_grideditor( + grideditor.QueryEditor( + self.master, + message.get_query().lst, + self.set_query, message + ) + ) + elif part == "u": + signals.status_prompt.send( + prompt = "URL", + text = message.url, + callback = self.set_url + ) + elif part == "m": + signals.status_prompt_onekey.send( + prompt = "Method", + keys = common.METHOD_OPTIONS, + callback = self.edit_method + ) + elif part == "o": + signals.status_prompt.send( + prompt = "Code", + text = str(message.status_code), + callback = self.set_resp_code + ) + elif part == "m": + signals.status_prompt.send( + prompt = "Message", + text = message.msg, + callback = self.set_resp_msg + ) + signals.flow_change.send(self, flow = self.flow) + + def _view_nextprev_flow(self, np, flow): + try: + idx = self.state.view.index(flow) + except IndexError: + return + if np == "next": + new_flow, new_idx = self.state.get_next(idx) + else: + new_flow, new_idx = self.state.get_prev(idx) + if new_flow is None: + signals.status_message.send(message="No more flows!") + else: + signals.pop_view_state.send(self) + self.master.view_flow(new_flow, self.tab_offset) + + def view_next_flow(self, flow): + return self._view_nextprev_flow("next", flow) + + def view_prev_flow(self, flow): + return self._view_nextprev_flow("prev", flow) + + def change_this_display_mode(self, t): + self.state.add_flow_setting( + self.flow, + (self.tab_offset, "prettyview"), + contentviews.get_by_shortcut(t) + ) + signals.flow_change.send(self, flow = self.flow) + + def delete_body(self, t): + if t == "m": + val = CONTENT_MISSING + else: + val = None + if self.tab_offset == TAB_REQ: + self.flow.request.content = val + else: + self.flow.response.content = val + signals.flow_change.send(self, flow = self.flow) + + def keypress(self, size, key): + key = super(self.__class__, self).keypress(size, key) + + if key == " ": + self.view_next_flow(self.flow) + return + + key = common.shortcuts(key) + if self.tab_offset == TAB_REQ: + conn = self.flow.request + elif self.tab_offset == TAB_RESP: + conn = self.flow.response + else: + conn = None + + if key in ("up", "down", "page up", "page down"): + # Why doesn't this just work?? + self._w.keypress(size, key) + elif key == "a": + self.flow.accept_intercept(self.master) + signals.flow_change.send(self, flow = self.flow) + elif key == "A": + self.master.accept_all() + signals.flow_change.send(self, flow = self.flow) + elif key == "d": + if self.state.flow_count() == 1: + self.master.view_flowlist() + elif self.state.view.index(self.flow) == len(self.state.view) - 1: + self.view_prev_flow(self.flow) + else: + self.view_next_flow(self.flow) + f = self.flow + f.kill(self.master) + self.state.delete_flow(f) + elif key == "D": + f = self.master.duplicate_flow(self.flow) + self.master.view_flow(f) + signals.status_message.send(message="Duplicated.") + elif key == "p": + self.view_prev_flow(self.flow) + elif key == "r": + r = self.master.replay_request(self.flow) + if r: + signals.status_message.send(message=r) + signals.flow_change.send(self, flow = self.flow) + elif key == "V": + if not self.flow.modified(): + signals.status_message.send(message="Flow not modified.") + return + self.state.revert(self.flow) + signals.flow_change.send(self, flow = self.flow) + signals.status_message.send(message="Reverted.") + elif key == "W": + signals.status_prompt_path.send( + prompt = "Save this flow", + callback = self.master.save_one_flow, + args = (self.flow,) + ) + elif key == "E": + signals.status_prompt_onekey.send( + self, + prompt = "Export", + keys = ( + ("as curl command", "c"), + ("as python code", "p"), + ("as raw request", "r"), + ), + callback = common.export_prompt, + args = (self.flow,) + ) + elif key == "|": + signals.status_prompt_path.send( + prompt = "Send flow to script", + callback = self.master.run_script_once, + args = (self.flow,) + ) + + if not conn and key in set(list("befgmxvz")): + signals.status_message.send( + message = "Tab to the request or response", + expire = 1 + ) + elif conn: + if key == "b": + if self.tab_offset == TAB_REQ: + common.ask_save_body( + "q", self.master, self.state, self.flow + ) + else: + common.ask_save_body( + "s", self.master, self.state, self.flow + ) + elif key == "e": + if self.tab_offset == TAB_REQ: + signals.status_prompt_onekey.send( + prompt = "Edit request", + keys = ( + ("cookies", "c"), + ("query", "q"), + ("path", "p"), + ("url", "u"), + ("header", "h"), + ("form", "f"), + ("raw body", "r"), + ("method", "m"), + ), + callback = self.edit + ) + else: + signals.status_prompt_onekey.send( + prompt = "Edit response", + keys = ( + ("cookies", "c"), + ("code", "o"), + ("message", "m"), + ("header", "h"), + ("raw body", "r"), + ), + callback = self.edit + ) + key = None + elif key == "f": + signals.status_message.send(message="Loading all body data...") + self.state.add_flow_setting( + self.flow, + (self.tab_offset, "fullcontents"), + True + ) + signals.flow_change.send(self, flow = self.flow) + signals.status_message.send(message="") + elif key == "P": + if self.tab_offset == TAB_REQ: + scope = "q" + else: + scope = "s" + common.ask_copy_part(scope, self.flow, self.master, self.state) + elif key == "m": + p = list(contentviews.view_prompts) + p.insert(0, ("Clear", "C")) + signals.status_prompt_onekey.send( + self, + prompt = "Display mode", + keys = p, + callback = self.change_this_display_mode + ) + key = None + elif key == "x": + signals.status_prompt_onekey.send( + prompt = "Delete body", + keys = ( + ("completely", "c"), + ("mark as missing", "m"), + ), + callback = self.delete_body + ) + key = None + elif key == "v": + if conn.content: + t = conn.headers.get("content-type") + if "EDITOR" in os.environ or "PAGER" in os.environ: + self.master.spawn_external_viewer(conn.content, t) + else: + signals.status_message.send( + message = "Error! Set $EDITOR or $PAGER." + ) + elif key == "z": + self.flow.backup() + e = conn.headers.get("content-encoding", "identity") + if e != "identity": + if not conn.decode(): + signals.status_message.send( + message = "Could not decode - invalid data?" + ) + else: + signals.status_prompt_onekey.send( + prompt = "Select encoding: ", + keys = ( + ("gzip", "z"), + ("deflate", "d"), + ), + callback = self.encode_callback, + args = (conn,) + ) + signals.flow_change.send(self, flow = self.flow) + return key + + def encode_callback(self, key, conn): + encoding_map = { + "z": "gzip", + "d": "deflate", + } + conn.encode(encoding_map[key]) + signals.flow_change.send(self, flow = self.flow) diff --git a/mitmproxy/console/grideditor.py b/mitmproxy/console/grideditor.py new file mode 100644 index 00000000..a11c962c --- /dev/null +++ b/mitmproxy/console/grideditor.py @@ -0,0 +1,716 @@ +from __future__ import absolute_import + +import copy +import re +import os +import urwid + +from netlib import odict +from netlib.http import user_agents + +from . import common, signals +from .. import utils, filt, script + + +FOOTER = [ + ('heading_key', "enter"), ":edit ", + ('heading_key', "q"), ":back ", +] +FOOTER_EDITING = [ + ('heading_key', "esc"), ":stop editing ", +] + + +class TextColumn: + subeditor = None + + def __init__(self, heading): + self.heading = heading + + def text(self, obj): + return SEscaped(obj or "") + + def blank(self): + return "" + + def keypress(self, key, editor): + if key == "r": + if editor.walker.get_current_value() is not None: + signals.status_prompt_path.send( + self, + prompt = "Read file", + callback = editor.read_file + ) + elif key == "R": + if editor.walker.get_current_value() is not None: + signals.status_prompt_path.send( + editor, + prompt = "Read unescaped file", + callback = editor.read_file, + args = (True,) + ) + elif key == "e": + o = editor.walker.get_current_value() + if o is not None: + n = editor.master.spawn_editor(o.encode("string-escape")) + n = utils.clean_hanging_newline(n) + editor.walker.set_current_value(n, False) + editor.walker._modified() + elif key in ["enter"]: + editor.walker.start_edit() + else: + return key + + +class SubgridColumn: + + def __init__(self, heading, subeditor): + self.heading = heading + self.subeditor = subeditor + + def text(self, obj): + p = http_cookies._format_pairs(obj, sep="\n") + return urwid.Text(p) + + def blank(self): + return [] + + def keypress(self, key, editor): + if key in "rRe": + signals.status_message.send( + self, + message = "Press enter to edit this field.", + expire = 1000 + ) + return + elif key in ["enter"]: + editor.master.view_grideditor( + self.subeditor( + editor.master, + editor.walker.get_current_value(), + editor.set_subeditor_value, + editor.walker.focus, + editor.walker.focus_col + ) + ) + else: + return key + + +class SEscaped(urwid.WidgetWrap): + + def __init__(self, txt): + txt = txt.encode("string-escape") + w = urwid.Text(txt, wrap="any") + urwid.WidgetWrap.__init__(self, w) + + def get_text(self): + return self._w.get_text()[0] + + def keypress(self, size, key): + return key + + def selectable(self): + return True + + +class SEdit(urwid.WidgetWrap): + + def __init__(self, txt): + txt = txt.encode("string-escape") + w = urwid.Edit(edit_text=txt, wrap="any", multiline=True) + w = urwid.AttrWrap(w, "editfield") + urwid.WidgetWrap.__init__(self, w) + + def get_text(self): + return self._w.get_text()[0].strip() + + def selectable(self): + return True + + +class GridRow(urwid.WidgetWrap): + + def __init__(self, focused, editing, editor, values): + self.focused, self.editing, self.editor = focused, editing, editor + + errors = values[1] + self.fields = [] + for i, v in enumerate(values[0]): + if focused == i and editing: + self.editing = SEdit(v) + self.fields.append(self.editing) + else: + w = self.editor.columns[i].text(v) + if focused == i: + if i in errors: + w = urwid.AttrWrap(w, "focusfield_error") + else: + w = urwid.AttrWrap(w, "focusfield") + elif i in errors: + w = urwid.AttrWrap(w, "field_error") + self.fields.append(w) + + fspecs = self.fields[:] + if len(self.fields) > 1: + fspecs[0] = ("fixed", self.editor.first_width + 2, fspecs[0]) + w = urwid.Columns( + fspecs, + dividechars = 2 + ) + if focused is not None: + w.set_focus_column(focused) + urwid.WidgetWrap.__init__(self, w) + + def get_edit_value(self): + return self.editing.get_text() + + def keypress(self, s, k): + if self.editing: + w = self._w.column_widths(s)[self.focused] + k = self.editing.keypress((w,), k) + return k + + def selectable(self): + return True + + +class GridWalker(urwid.ListWalker): + + """ + Stores rows as a list of (rows, errors) tuples, where rows is a list + and errors is a set with an entry of each offset in rows that is an + error. + """ + + def __init__(self, lst, editor): + self.lst = [(i, set([])) for i in lst] + self.editor = editor + self.focus = 0 + self.focus_col = 0 + self.editing = False + + def _modified(self): + self.editor.show_empty_msg() + return urwid.ListWalker._modified(self) + + def add_value(self, lst): + self.lst.append((lst[:], set([]))) + self._modified() + + def get_current_value(self): + if self.lst: + return self.lst[self.focus][0][self.focus_col] + + def set_current_value(self, val, unescaped): + if not unescaped: + try: + val = val.decode("string-escape") + except ValueError: + signals.status_message.send( + self, + message = "Invalid Python-style string encoding.", + expire = 1000 + ) + return + errors = self.lst[self.focus][1] + emsg = self.editor.is_error(self.focus_col, val) + if emsg: + signals.status_message.send(message = emsg, expire = 1) + errors.add(self.focus_col) + else: + errors.discard(self.focus_col) + self.set_value(val, self.focus, self.focus_col, errors) + + def set_value(self, val, focus, focus_col, errors=None): + if not errors: + errors = set([]) + row = list(self.lst[focus][0]) + row[focus_col] = val + self.lst[focus] = [tuple(row), errors] + self._modified() + + def delete_focus(self): + if self.lst: + del self.lst[self.focus] + self.focus = min(len(self.lst) - 1, self.focus) + self._modified() + + def _insert(self, pos): + self.focus = pos + self.lst.insert( + self.focus, + [ + [c.blank() for c in self.editor.columns], set([]) + ] + ) + self.focus_col = 0 + self.start_edit() + + def insert(self): + return self._insert(self.focus) + + def add(self): + return self._insert(min(self.focus + 1, len(self.lst))) + + def start_edit(self): + col = self.editor.columns[self.focus_col] + if self.lst and not col.subeditor: + self.editing = GridRow( + self.focus_col, True, self.editor, self.lst[self.focus] + ) + self.editor.master.loop.widget.footer.update(FOOTER_EDITING) + self._modified() + + def stop_edit(self): + if self.editing: + self.editor.master.loop.widget.footer.update(FOOTER) + self.set_current_value(self.editing.get_edit_value(), False) + self.editing = False + self._modified() + + def left(self): + self.focus_col = max(self.focus_col - 1, 0) + self._modified() + + def right(self): + self.focus_col = min(self.focus_col + 1, len(self.editor.columns) - 1) + self._modified() + + def tab_next(self): + self.stop_edit() + if self.focus_col < len(self.editor.columns) - 1: + self.focus_col += 1 + elif self.focus != len(self.lst) - 1: + self.focus_col = 0 + self.focus += 1 + self._modified() + + def get_focus(self): + if self.editing: + return self.editing, self.focus + elif self.lst: + return GridRow( + self.focus_col, + False, + self.editor, + self.lst[self.focus] + ), self.focus + else: + return None, None + + def set_focus(self, focus): + self.stop_edit() + self.focus = focus + self._modified() + + def get_next(self, pos): + if pos + 1 >= len(self.lst): + return None, None + return GridRow(None, False, self.editor, self.lst[pos + 1]), pos + 1 + + def get_prev(self, pos): + if pos - 1 < 0: + return None, None + return GridRow(None, False, self.editor, self.lst[pos - 1]), pos - 1 + + +class GridListBox(urwid.ListBox): + + def __init__(self, lw): + urwid.ListBox.__init__(self, lw) + + +FIRST_WIDTH_MAX = 40 +FIRST_WIDTH_MIN = 20 + + +class GridEditor(urwid.WidgetWrap): + title = None + columns = None + + def __init__(self, master, value, callback, *cb_args, **cb_kwargs): + value = self.data_in(copy.deepcopy(value)) + self.master, self.value, self.callback = master, value, callback + self.cb_args, self.cb_kwargs = cb_args, cb_kwargs + + first_width = 20 + if value: + for r in value: + assert len(r) == len(self.columns) + first_width = max(len(r), first_width) + self.first_width = min(first_width, FIRST_WIDTH_MAX) + + title = urwid.Text(self.title) + title = urwid.Padding(title, align="left", width=("relative", 100)) + title = urwid.AttrWrap(title, "heading") + + headings = [] + for i, col in enumerate(self.columns): + c = urwid.Text(col.heading) + if i == 0 and len(self.columns) > 1: + headings.append(("fixed", first_width + 2, c)) + else: + headings.append(c) + h = urwid.Columns( + headings, + dividechars = 2 + ) + h = urwid.AttrWrap(h, "heading") + + self.walker = GridWalker(self.value, self) + self.lb = GridListBox(self.walker) + self._w = urwid.Frame( + self.lb, + header = urwid.Pile([title, h]) + ) + self.master.loop.widget.footer.update("") + self.show_empty_msg() + + def show_empty_msg(self): + if self.walker.lst: + self._w.set_footer(None) + else: + self._w.set_footer( + urwid.Text( + [ + ("highlight", "No values. Press "), + ("key", "a"), + ("highlight", " to add some."), + ] + ) + ) + + def encode(self, s): + if not self.encoding: + return s + try: + return s.encode(self.encoding) + except ValueError: + return None + + def read_file(self, p, unescaped=False): + if p: + try: + p = os.path.expanduser(p) + d = file(p, "rb").read() + self.walker.set_current_value(d, unescaped) + self.walker._modified() + except IOError as v: + return str(v) + + def set_subeditor_value(self, val, focus, focus_col): + self.walker.set_value(val, focus, focus_col) + + def keypress(self, size, key): + if self.walker.editing: + if key in ["esc"]: + self.walker.stop_edit() + elif key == "tab": + pf, pfc = self.walker.focus, self.walker.focus_col + self.walker.tab_next() + if self.walker.focus == pf and self.walker.focus_col != pfc: + self.walker.start_edit() + else: + self._w.keypress(size, key) + return None + + key = common.shortcuts(key) + column = self.columns[self.walker.focus_col] + if key in ["q", "esc"]: + res = [] + for i in self.walker.lst: + if not i[1] and any([x for x in i[0]]): + res.append(i[0]) + self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs) + signals.pop_view_state.send(self) + elif key == "g": + self.walker.set_focus(0) + elif key == "G": + self.walker.set_focus(len(self.walker.lst) - 1) + elif key in ["h", "left"]: + self.walker.left() + elif key in ["l", "right"]: + self.walker.right() + elif key == "tab": + self.walker.tab_next() + elif key == "a": + self.walker.add() + elif key == "A": + self.walker.insert() + elif key == "d": + self.walker.delete_focus() + elif column.keypress(key, self) and not self.handle_key(key): + return self._w.keypress(size, key) + + def data_out(self, data): + """ + Called on raw list data, before data is returned through the + callback. + """ + return data + + def data_in(self, data): + """ + Called to prepare provided data. + """ + return data + + def is_error(self, col, val): + """ + Return False, or a string error message. + """ + return False + + def handle_key(self, key): + return False + + def make_help(self): + text = [] + text.append(urwid.Text([("text", "Editor control:\n")])) + keys = [ + ("A", "insert row before cursor"), + ("a", "add row after cursor"), + ("d", "delete row"), + ("e", "spawn external editor on current field"), + ("q", "save changes and exit editor"), + ("r", "read value from file"), + ("R", "read unescaped value from file"), + ("esc", "save changes and exit editor"), + ("tab", "next field"), + ("enter", "edit field"), + ] + text.extend( + common.format_keyvals(keys, key="key", val="text", indent=4) + ) + text.append( + urwid.Text( + [ + "\n", + ("text", "Values are escaped Python-style strings.\n"), + ] + ) + ) + return text + + +class QueryEditor(GridEditor): + title = "Editing query" + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] + + +class HeaderEditor(GridEditor): + title = "Editing headers" + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] + + def make_help(self): + h = GridEditor.make_help(self) + text = [] + text.append(urwid.Text([("text", "Special keys:\n")])) + keys = [ + ("U", "add User-Agent header"), + ] + text.extend( + common.format_keyvals(keys, key="key", val="text", indent=4) + ) + text.append(urwid.Text([("text", "\n")])) + text.extend(h) + return text + + def set_user_agent(self, k): + ua = user_agents.get_by_shortcut(k) + if ua: + self.walker.add_value( + [ + "User-Agent", + ua[2] + ] + ) + + def handle_key(self, key): + if key == "U": + signals.status_prompt_onekey.send( + prompt = "Add User-Agent header:", + keys = [(i[0], i[1]) for i in user_agents.UASTRINGS], + callback = self.set_user_agent, + ) + return True + + +class URLEncodedFormEditor(GridEditor): + title = "Editing URL-encoded form" + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] + + +class ReplaceEditor(GridEditor): + title = "Editing replacement patterns" + columns = [ + TextColumn("Filter"), + TextColumn("Regex"), + TextColumn("Replacement"), + ] + + def is_error(self, col, val): + if col == 0: + if not filt.parse(val): + return "Invalid filter specification." + elif col == 1: + try: + re.compile(val) + except re.error: + return "Invalid regular expression." + return False + + +class SetHeadersEditor(GridEditor): + title = "Editing header set patterns" + columns = [ + TextColumn("Filter"), + TextColumn("Header"), + TextColumn("Value"), + ] + + def is_error(self, col, val): + if col == 0: + if not filt.parse(val): + return "Invalid filter specification" + return False + + def make_help(self): + h = GridEditor.make_help(self) + text = [] + text.append(urwid.Text([("text", "Special keys:\n")])) + keys = [ + ("U", "add User-Agent header"), + ] + text.extend( + common.format_keyvals(keys, key="key", val="text", indent=4) + ) + text.append(urwid.Text([("text", "\n")])) + text.extend(h) + return text + + def set_user_agent(self, k): + ua = user_agents.get_by_shortcut(k) + if ua: + self.walker.add_value( + [ + ".*", + "User-Agent", + ua[2] + ] + ) + + def handle_key(self, key): + if key == "U": + signals.status_prompt_onekey.send( + prompt = "Add User-Agent header:", + keys = [(i[0], i[1]) for i in user_agents.UASTRINGS], + callback = self.set_user_agent, + ) + return True + + +class PathEditor(GridEditor): + title = "Editing URL path components" + columns = [ + TextColumn("Component"), + ] + + def data_in(self, data): + return [[i] for i in data] + + def data_out(self, data): + return [i[0] for i in data] + + +class ScriptEditor(GridEditor): + title = "Editing scripts" + columns = [ + TextColumn("Command"), + ] + + def is_error(self, col, val): + try: + script.Script.parse_command(val) + except script.ScriptException as v: + return str(v) + + +class HostPatternEditor(GridEditor): + title = "Editing host patterns" + columns = [ + TextColumn("Regex (matched on hostname:port / ip:port)") + ] + + def is_error(self, col, val): + try: + re.compile(val, re.IGNORECASE) + except re.error as e: + return "Invalid regex: %s" % str(e) + + def data_in(self, data): + return [[i] for i in data] + + def data_out(self, data): + return [i[0] for i in data] + + +class CookieEditor(GridEditor): + title = "Editing request Cookie header" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + ] + + +class CookieAttributeEditor(GridEditor): + title = "Editing Set-Cookie attributes" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + ] + + def data_out(self, data): + ret = [] + for i in data: + if not i[1]: + ret.append([i[0], None]) + else: + ret.append(i) + return ret + + +class SetCookieEditor(GridEditor): + title = "Editing response SetCookie header" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + SubgridColumn("Attributes", CookieAttributeEditor), + ] + + def data_in(self, data): + flattened = [] + for k, v in data.items(): + flattened.append([k, v[0], v[1].lst]) + return flattened + + def data_out(self, data): + vals = [] + for i in data: + vals.append( + [ + i[0], + [i[1], odict.ODictCaseless(i[2])] + ] + ) + return odict.ODict(vals) diff --git a/mitmproxy/console/help.py b/mitmproxy/console/help.py new file mode 100644 index 00000000..0c264ebf --- /dev/null +++ b/mitmproxy/console/help.py @@ -0,0 +1,117 @@ +from __future__ import absolute_import + +import urwid + +from . import common, signals +from .. import filt, version + +footer = [ + ("heading", 'mitmproxy v%s ' % version.VERSION), + ('heading_key', "q"), ":back ", +] + + +class HelpView(urwid.ListBox): + + def __init__(self, help_context): + self.help_context = help_context or [] + urwid.ListBox.__init__( + self, + self.helptext() + ) + + def helptext(self): + text = [] + text.append(urwid.Text([("head", "This view:\n")])) + text.extend(self.help_context) + + text.append(urwid.Text([("head", "\n\nMovement:\n")])) + keys = [ + ("j, k", "down, up"), + ("h, l", "left, right (in some contexts)"), + ("g, G", "go to beginning, end"), + ("space", "page down"), + ("pg up/down", "page up/down"), + ("ctrl+b/ctrl+f", "page up/down"), + ("arrows", "up, down, left, right"), + ] + text.extend( + common.format_keyvals( + keys, + key="key", + val="text", + indent=4)) + + text.append(urwid.Text([("head", "\n\nGlobal keys:\n")])) + keys = [ + ("c", "client replay of HTTP requests"), + ("i", "set interception pattern"), + ("o", "options"), + ("q", "quit / return to previous page"), + ("Q", "quit without confirm prompt"), + ("S", "server replay of HTTP responses"), + ] + text.extend( + common.format_keyvals(keys, key="key", val="text", indent=4) + ) + + text.append(urwid.Text([("head", "\n\nFilter expressions:\n")])) + f = [] + for i in filt.filt_unary: + f.append( + ("~%s" % i.code, i.help) + ) + for i in filt.filt_rex: + f.append( + ("~%s regex" % i.code, i.help) + ) + for i in filt.filt_int: + f.append( + ("~%s int" % i.code, i.help) + ) + f.sort() + f.extend( + [ + ("!", "unary not"), + ("&", "and"), + ("|", "or"), + ("(...)", "grouping"), + ] + ) + text.extend(common.format_keyvals(f, key="key", val="text", indent=4)) + + text.append( + urwid.Text( + [ + "\n", + ("text", " Regexes are Python-style.\n"), + ("text", " Regexes can be specified as quoted strings.\n"), + ("text", " Header matching (~h, ~hq, ~hs) is against a string of the form \"name: value\".\n"), + ("text", " Expressions with no operators are regex matches against URL.\n"), + ("text", " Default binary operator is &.\n"), + ("head", "\n Examples:\n"), + ] + ) + ) + examples = [ + ("google\.com", "Url containing \"google.com"), + ("~q ~b test", "Requests where body contains \"test\""), + ("!(~q & ~t \"text/html\")", "Anything but requests with a text/html content type."), + ] + text.extend( + common.format_keyvals(examples, key="key", val="text", indent=4) + ) + return text + + def keypress(self, size, key): + key = common.shortcuts(key) + if key == "q": + signals.pop_view_state.send(self) + return None + elif key == "?": + key = None + elif key == "g": + self.set_focus(0) + elif key == "G": + self.set_focus(len(self.body.contents)) + return urwid.ListBox.keypress(self, size, key) diff --git a/mitmproxy/console/options.py b/mitmproxy/console/options.py new file mode 100644 index 00000000..5c9e0cc9 --- /dev/null +++ b/mitmproxy/console/options.py @@ -0,0 +1,271 @@ +import urwid + +from .. import contentviews +from . import common, signals, grideditor +from . import select, palettes + +footer = [ + ('heading_key', "enter/space"), ":toggle ", + ('heading_key', "C"), ":clear all ", +] + + +def _mkhelp(): + text = [] + keys = [ + ("enter/space", "activate option"), + ("C", "clear all options"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + + +class Options(urwid.WidgetWrap): + + def __init__(self, master): + self.master = master + self.lb = select.Select( + [ + select.Heading("Traffic Manipulation"), + select.Option( + "Header Set Patterns", + "H", + lambda: master.setheaders.count(), + self.setheaders + ), + select.Option( + "Ignore Patterns", + "I", + lambda: master.server.config.check_ignore, + self.ignorepatterns + ), + select.Option( + "Replacement Patterns", + "R", + lambda: master.replacehooks.count(), + self.replacepatterns + ), + select.Option( + "Scripts", + "S", + lambda: master.scripts, + self.scripts + ), + + select.Heading("Interface"), + select.Option( + "Default Display Mode", + "M", + self.has_default_displaymode, + self.default_displaymode + ), + select.Option( + "Palette", + "P", + lambda: self.master.palette != palettes.DEFAULT, + self.palette + ), + select.Option( + "Show Host", + "w", + lambda: master.showhost, + self.toggle_showhost + ), + + select.Heading("Network"), + select.Option( + "No Upstream Certs", + "U", + lambda: master.server.config.no_upstream_cert, + self.toggle_upstream_cert + ), + select.Option( + "TCP Proxying", + "T", + lambda: master.server.config.check_tcp, + self.tcp_proxy + ), + + select.Heading("Utility"), + select.Option( + "Anti-Cache", + "a", + lambda: master.anticache, + self.toggle_anticache + ), + select.Option( + "Anti-Compression", + "o", + lambda: master.anticomp, + self.toggle_anticomp + ), + select.Option( + "Kill Extra", + "x", + lambda: master.killextra, + self.toggle_killextra + ), + select.Option( + "No Refresh", + "f", + lambda: not master.refresh_server_playback, + self.toggle_refresh_server_playback + ), + select.Option( + "Sticky Auth", + "A", + lambda: master.stickyauth_txt, + self.sticky_auth + ), + select.Option( + "Sticky Cookies", + "t", + lambda: master.stickycookie_txt, + self.sticky_cookie + ), + ] + ) + title = urwid.Text("Options") + title = urwid.Padding(title, align="left", width=("relative", 100)) + title = urwid.AttrWrap(title, "heading") + self._w = urwid.Frame( + self.lb, + header = title + ) + self.master.loop.widget.footer.update("") + signals.update_settings.connect(self.sig_update_settings) + + def sig_update_settings(self, sender): + self.lb.walker._modified() + + def keypress(self, size, key): + if key == "C": + self.clearall() + return None + return super(self.__class__, self).keypress(size, key) + + def clearall(self): + self.master.anticache = False + self.master.anticomp = False + self.master.killextra = False + self.master.showhost = False + self.master.refresh_server_playback = True + self.master.server.config.no_upstream_cert = False + self.master.setheaders.clear() + self.master.replacehooks.clear() + self.master.set_ignore_filter([]) + self.master.set_tcp_filter([]) + self.master.scripts = [] + self.master.set_stickyauth(None) + self.master.set_stickycookie(None) + self.master.state.default_body_view = contentviews.get("Auto") + + signals.update_settings.send(self) + signals.status_message.send( + message = "All select.Options cleared", + expire = 1 + ) + + def toggle_anticache(self): + self.master.anticache = not self.master.anticache + + def toggle_anticomp(self): + self.master.anticomp = not self.master.anticomp + + def toggle_killextra(self): + self.master.killextra = not self.master.killextra + + def toggle_showhost(self): + self.master.showhost = not self.master.showhost + + def toggle_refresh_server_playback(self): + self.master.refresh_server_playback = not self.master.refresh_server_playback + + def toggle_upstream_cert(self): + self.master.server.config.no_upstream_cert = not self.master.server.config.no_upstream_cert + signals.update_settings.send(self) + + def setheaders(self): + def _set(*args, **kwargs): + self.master.setheaders.set(*args, **kwargs) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.SetHeadersEditor( + self.master, + self.master.setheaders.get_specs(), + _set + ) + ) + + def ignorepatterns(self): + def _set(ignore): + self.master.set_ignore_filter(ignore) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + self.master.get_ignore_filter(), + _set + ) + ) + + def replacepatterns(self): + def _set(*args, **kwargs): + self.master.replacehooks.set(*args, **kwargs) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.ReplaceEditor( + self.master, + self.master.replacehooks.get_specs(), + _set + ) + ) + + def scripts(self): + self.master.view_grideditor( + grideditor.ScriptEditor( + self.master, + [[i.command] for i in self.master.scripts], + self.master.edit_scripts + ) + ) + + def default_displaymode(self): + signals.status_prompt_onekey.send( + prompt = "Global default display mode", + keys = contentviews.view_prompts, + callback = self.master.change_default_display_mode + ) + + def has_default_displaymode(self): + return self.master.state.default_body_view.name != "Auto" + + def tcp_proxy(self): + def _set(tcp): + self.master.set_tcp_filter(tcp) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + self.master.get_tcp_filter(), + _set + ) + ) + + def sticky_auth(self): + signals.status_prompt.send( + prompt = "Sticky auth filter", + text = self.master.stickyauth_txt, + callback = self.master.set_stickyauth + ) + + def sticky_cookie(self): + signals.status_prompt.send( + prompt = "Sticky cookie filter", + text = self.master.stickycookie_txt, + callback = self.master.set_stickycookie + ) + + def palette(self): + self.master.view_palette_picker() diff --git a/mitmproxy/console/palettepicker.py b/mitmproxy/console/palettepicker.py new file mode 100644 index 00000000..51ad0606 --- /dev/null +++ b/mitmproxy/console/palettepicker.py @@ -0,0 +1,82 @@ +import urwid + +from . import select, common, palettes, signals + +footer = [ + ('heading_key', "enter/space"), ":select", +] + + +def _mkhelp(): + text = [] + keys = [ + ("enter/space", "select"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + + +class PalettePicker(urwid.WidgetWrap): + + def __init__(self, master): + self.master = master + low, high = [], [] + for k, v in palettes.palettes.items(): + if v.high: + high.append(k) + else: + low.append(k) + high.sort() + low.sort() + + options = [ + select.Heading("High Colour") + ] + + def mkopt(name): + return select.Option( + i, + None, + lambda: self.master.palette == name, + lambda: self.select(name) + ) + + for i in high: + options.append(mkopt(i)) + options.append(select.Heading("Low Colour")) + for i in low: + options.append(mkopt(i)) + + options.extend( + [ + select.Heading("Options"), + select.Option( + "Transparent", + "T", + lambda: master.palette_transparent, + self.toggle_palette_transparent + ) + ] + ) + + self.lb = select.Select(options) + title = urwid.Text("Palettes") + title = urwid.Padding(title, align="left", width=("relative", 100)) + title = urwid.AttrWrap(title, "heading") + self._w = urwid.Frame( + self.lb, + header = title + ) + signals.update_settings.connect(self.sig_update_settings) + + def sig_update_settings(self, sender): + self.lb.walker._modified() + + def select(self, name): + self.master.set_palette(name) + + def toggle_palette_transparent(self): + self.master.palette_transparent = not self.master.palette_transparent + self.master.set_palette(self.master.palette) + signals.update_settings.send(self) diff --git a/mitmproxy/console/palettes.py b/mitmproxy/console/palettes.py new file mode 100644 index 00000000..bd370181 --- /dev/null +++ b/mitmproxy/console/palettes.py @@ -0,0 +1,326 @@ +# Low-color themes should ONLY use the standard foreground and background +# colours listed here: +# +# http://urwid.org/manual/displayattributes.html +# + + +class Palette: + _fields = [ + 'background', + 'title', + + # Status bar & heading + 'heading', 'heading_key', 'heading_inactive', + + # Help + 'key', 'head', 'text', + + # Options + 'option_selected', 'option_active', 'option_active_selected', + 'option_selected_key', + + # List and Connections + 'method', 'focus', + 'code_200', 'code_300', 'code_400', 'code_500', 'code_other', + 'error', + 'header', 'highlight', 'intercept', 'replay', 'mark', + + # Hex view + 'offset', + + # Grid Editor + 'focusfield', 'focusfield_error', 'field_error', 'editfield', + ] + high = None + + def palette(self, transparent): + l = [] + highback, lowback = None, None + if not transparent: + if self.high and self.high.get("background"): + highback = self.high["background"][1] + lowback = self.low["background"][1] + + for i in self._fields: + if transparent and i == "background": + l.append(["background", "default", "default"]) + else: + v = [i] + low = list(self.low[i]) + if lowback and low[1] == "default": + low[1] = lowback + v.extend(low) + if self.high and i in self.high: + v.append(None) + high = list(self.high[i]) + if highback and high[1] == "default": + high[1] = highback + v.extend(high) + elif highback and self.low[i][1] == "default": + high = [None, low[0], highback] + v.extend(high) + l.append(tuple(v)) + return l + + +class LowDark(Palette): + + """ + Low-color dark background + """ + low = dict( + background = ('white', 'black'), + title = ('white,bold', 'default'), + + # Status bar & heading + heading = ('white', 'dark blue'), + heading_key = ('light cyan', 'dark blue'), + heading_inactive = ('dark gray', 'light gray'), + + # Help + key = ('light cyan', 'default'), + head = ('white,bold', 'default'), + text = ('light gray', 'default'), + + # Options + option_selected = ('black', 'light gray'), + option_selected_key = ('light cyan', 'light gray'), + option_active = ('light red', 'default'), + option_active_selected = ('light red', 'light gray'), + + # List and Connections + method = ('dark cyan', 'default'), + focus = ('yellow', 'default'), + + code_200 = ('dark green', 'default'), + code_300 = ('light blue', 'default'), + code_400 = ('light red', 'default'), + code_500 = ('light red', 'default'), + code_other = ('dark red', 'default'), + + error = ('light red', 'default'), + + header = ('dark cyan', 'default'), + highlight = ('white,bold', 'default'), + intercept = ('brown', 'default'), + replay = ('light green', 'default'), + mark = ('light red', 'default'), + + # Hex view + offset = ('dark cyan', 'default'), + + # Grid Editor + focusfield = ('black', 'light gray'), + focusfield_error = ('dark red', 'light gray'), + field_error = ('dark red', 'default'), + editfield = ('white', 'default'), + ) + + +class Dark(LowDark): + high = dict( + heading_inactive = ('g58', 'g11'), + intercept = ('#f60', 'default'), + + option_selected = ('g85', 'g45'), + option_selected_key = ('light cyan', 'g50'), + option_active_selected = ('light red', 'g50'), + ) + + +class LowLight(Palette): + + """ + Low-color light background + """ + low = dict( + background = ('black', 'white'), + title = ('dark magenta', 'default'), + + # Status bar & heading + heading = ('white', 'black'), + heading_key = ('dark blue', 'black'), + heading_inactive = ('black', 'light gray'), + + # Help + key = ('dark blue', 'default'), + head = ('black', 'default'), + text = ('dark gray', 'default'), + + # Options + option_selected = ('black', 'light gray'), + option_selected_key = ('dark blue', 'light gray'), + option_active = ('light red', 'default'), + option_active_selected = ('light red', 'light gray'), + + # List and Connections + method = ('dark cyan', 'default'), + focus = ('black', 'default'), + + code_200 = ('dark green', 'default'), + code_300 = ('light blue', 'default'), + code_400 = ('dark red', 'default'), + code_500 = ('dark red', 'default'), + code_other = ('light red', 'default'), + + error = ('light red', 'default'), + + header = ('dark blue', 'default'), + highlight = ('black,bold', 'default'), + intercept = ('brown', 'default'), + replay = ('dark green', 'default'), + mark = ('dark red', 'default'), + + # Hex view + offset = ('dark blue', 'default'), + + # Grid Editor + focusfield = ('black', 'light gray'), + focusfield_error = ('dark red', 'light gray'), + field_error = ('dark red', 'black'), + editfield = ('black', 'default'), + ) + + +class Light(LowLight): + high = dict( + background = ('black', 'g100'), + heading = ('g99', '#08f'), + heading_key = ('#0ff,bold', '#08f'), + heading_inactive = ('g35', 'g85'), + replay = ('#0a0,bold', 'default'), + + option_selected = ('black', 'g85'), + option_selected_key = ('dark blue', 'g85'), + option_active_selected = ('light red', 'g85'), + ) + + +# Solarized palette in Urwid-style terminal high-colour offsets +# See: http://ethanschoonover.com/solarized +sol_base03 = "h234" +sol_base02 = "h235" +sol_base01 = "h240" +sol_base00 = "h241" +sol_base0 = "h244" +sol_base1 = "h245" +sol_base2 = "h254" +sol_base3 = "h230" +sol_yellow = "h136" +sol_orange = "h166" +sol_red = "h160" +sol_magenta = "h125" +sol_violet = "h61" +sol_blue = "h33" +sol_cyan = "h37" +sol_green = "h64" + + +class SolarizedLight(LowLight): + high = dict( + background = (sol_base00, sol_base3), + title = (sol_cyan, 'default'), + text = (sol_base00, 'default'), + + # Status bar & heading + heading = (sol_base2, sol_base02), + heading_key = (sol_blue, sol_base03), + heading_inactive = (sol_base03, sol_base1), + + # Help + key = (sol_blue, 'default',), + head = (sol_base00, 'default'), + + # Options + option_selected = (sol_base03, sol_base2), + option_selected_key = (sol_blue, sol_base2), + option_active = (sol_orange, 'default'), + option_active_selected = (sol_orange, sol_base2), + + # List and Connections + method = (sol_cyan, 'default'), + focus = (sol_base01, 'default'), + + code_200 = (sol_green, 'default'), + code_300 = (sol_blue, 'default'), + code_400 = (sol_orange, 'default',), + code_500 = (sol_red, 'default'), + code_other = (sol_magenta, 'default'), + + error = (sol_red, 'default'), + + header = (sol_blue, 'default'), + highlight = (sol_base01, 'default'), + intercept = (sol_red, 'default',), + replay = (sol_green, 'default',), + + # Hex view + offset = (sol_cyan, 'default'), + + # Grid Editor + focusfield = (sol_base00, sol_base2), + focusfield_error = (sol_red, sol_base2), + field_error = (sol_red, 'default'), + editfield = (sol_base01, 'default'), + ) + + +class SolarizedDark(LowDark): + high = dict( + background = (sol_base2, sol_base03), + title = (sol_blue, 'default'), + text = (sol_base1, 'default'), + + # Status bar & heading + heading = (sol_base2, sol_base01), + heading_key = (sol_blue + ",bold", sol_base01), + heading_inactive = (sol_base1, sol_base02), + + # Help + key = (sol_blue, 'default',), + head = (sol_base2, 'default'), + + # Options + option_selected = (sol_base03, sol_base00), + option_selected_key = (sol_blue, sol_base00), + option_active = (sol_orange, 'default'), + option_active_selected = (sol_orange, sol_base00), + + # List and Connections + method = (sol_cyan, 'default'), + focus = (sol_base1, 'default'), + + code_200 = (sol_green, 'default'), + code_300 = (sol_blue, 'default'), + code_400 = (sol_orange, 'default',), + code_500 = (sol_red, 'default'), + code_other = (sol_magenta, 'default'), + + error = (sol_red, 'default'), + + header = (sol_blue, 'default'), + highlight = (sol_base01, 'default'), + intercept = (sol_red, 'default',), + replay = (sol_green, 'default',), + + # Hex view + offset = (sol_cyan, 'default'), + + # Grid Editor + focusfield = (sol_base0, sol_base02), + focusfield_error = (sol_red, sol_base02), + field_error = (sol_red, 'default'), + editfield = (sol_base1, 'default'), + ) + + +DEFAULT = "dark" +palettes = { + "lowlight": LowLight(), + "lowdark": LowDark(), + "light": Light(), + "dark": Dark(), + "solarized_light": SolarizedLight(), + "solarized_dark": SolarizedDark(), +} diff --git a/mitmproxy/console/pathedit.py b/mitmproxy/console/pathedit.py new file mode 100644 index 00000000..4447070b --- /dev/null +++ b/mitmproxy/console/pathedit.py @@ -0,0 +1,71 @@ +import glob +import os.path + +import urwid + + +class _PathCompleter: + + def __init__(self, _testing=False): + """ + _testing: disables reloading of the lookup table to make testing + possible. + """ + self.lookup, self.offset = None, None + self.final = None + self._testing = _testing + + def reset(self): + self.lookup = None + self.offset = -1 + + def complete(self, txt): + """ + Returns the next completion for txt, or None if there is no + completion. + """ + path = os.path.expanduser(txt) + if not self.lookup: + if not self._testing: + # Lookup is a set of (display value, actual value) tuples. + self.lookup = [] + if os.path.isdir(path): + files = glob.glob(os.path.join(path, "*")) + prefix = txt + else: + files = glob.glob(path + "*") + prefix = os.path.dirname(txt) + prefix = prefix or "./" + for f in files: + display = os.path.join(prefix, os.path.basename(f)) + if os.path.isdir(f): + display += "/" + self.lookup.append((display, f)) + if not self.lookup: + self.final = path + return path + self.lookup.sort() + self.offset = -1 + self.lookup.append((txt, txt)) + self.offset += 1 + if self.offset >= len(self.lookup): + self.offset = 0 + ret = self.lookup[self.offset] + self.final = ret[1] + return ret[0] + + +class PathEdit(urwid.Edit, _PathCompleter): + + def __init__(self, *args, **kwargs): + urwid.Edit.__init__(self, *args, **kwargs) + _PathCompleter.__init__(self) + + def keypress(self, size, key): + if key == "tab": + comp = self.complete(self.get_edit_text()) + self.set_edit_text(comp) + self.set_edit_pos(len(comp)) + else: + self.reset() + return urwid.Edit.keypress(self, size, key) diff --git a/mitmproxy/console/searchable.py b/mitmproxy/console/searchable.py new file mode 100644 index 00000000..cff1f0a1 --- /dev/null +++ b/mitmproxy/console/searchable.py @@ -0,0 +1,93 @@ +import urwid + +from . import signals + + +class Highlight(urwid.AttrMap): + + def __init__(self, t): + urwid.AttrMap.__init__( + self, + urwid.Text(t.text), + "focusfield", + ) + self.backup = t + + +class Searchable(urwid.ListBox): + + def __init__(self, state, contents): + self.walker = urwid.SimpleFocusListWalker(contents) + urwid.ListBox.__init__(self, self.walker) + self.state = state + self.search_offset = 0 + self.current_highlight = None + self.search_term = None + + def keypress(self, size, key): + if key == "/": + signals.status_prompt.send( + prompt = "Search for", + text = "", + callback = self.set_search + ) + elif key == "n": + self.find_next(False) + elif key == "N": + self.find_next(True) + elif key == "g": + self.set_focus(0) + self.walker._modified() + elif key == "G": + self.set_focus(len(self.walker) - 1) + self.walker._modified() + else: + return super(self.__class__, self).keypress(size, key) + + def set_search(self, text): + self.state.last_search = text + self.search_term = text or None + self.find_next(False) + + def set_highlight(self, offset): + if self.current_highlight is not None: + old = self.body[self.current_highlight] + self.body[self.current_highlight] = old.backup + if offset is None: + self.current_highlight = None + else: + self.body[offset] = Highlight(self.body[offset]) + self.current_highlight = offset + + def get_text(self, w): + if isinstance(w, urwid.Text): + return w.text + elif isinstance(w, Highlight): + return w.backup.text + else: + return None + + def find_next(self, backwards): + if not self.search_term: + if self.state.last_search: + self.search_term = self.state.last_search + else: + self.set_highlight(None) + return + # Start search at focus + 1 + if backwards: + rng = xrange(len(self.body) - 1, -1, -1) + else: + rng = xrange(1, len(self.body) + 1) + for i in rng: + off = (self.focus_position + i) % len(self.body) + w = self.body[off] + txt = self.get_text(w) + if txt and self.search_term in txt: + self.set_highlight(off) + self.set_focus(off, coming_from="above") + self.body._modified() + return + else: + self.set_highlight(None) + signals.status_message.send(message="Search not found.", expire=1) diff --git a/mitmproxy/console/select.py b/mitmproxy/console/select.py new file mode 100644 index 00000000..928a7ca5 --- /dev/null +++ b/mitmproxy/console/select.py @@ -0,0 +1,120 @@ +import urwid + +from . import common + + +class _OptionWidget(urwid.WidgetWrap): + + def __init__(self, option, text, shortcut, active, focus): + self.option = option + textattr = "text" + keyattr = "key" + if focus and active: + textattr = "option_active_selected" + keyattr = "option_selected_key" + elif focus: + textattr = "option_selected" + keyattr = "option_selected_key" + elif active: + textattr = "option_active" + if shortcut: + text = common.highlight_key( + text, + shortcut, + textattr = textattr, + keyattr = keyattr + ) + opt = urwid.Text(text, align="left") + opt = urwid.AttrWrap(opt, textattr) + opt = urwid.Padding(opt, align = "center", width = 40) + urwid.WidgetWrap.__init__(self, opt) + + def keypress(self, size, key): + return key + + def selectable(self): + return True + + +class OptionWalker(urwid.ListWalker): + + def __init__(self, options): + urwid.ListWalker.__init__(self) + self.options = options + self.focus = 0 + + def set_focus(self, pos): + self.focus = pos + + def get_focus(self): + return self.options[self.focus].render(True), self.focus + + def get_next(self, pos): + if pos >= len(self.options) - 1: + return None, None + return self.options[pos + 1].render(False), pos + 1 + + def get_prev(self, pos): + if pos <= 0: + return None, None + return self.options[pos - 1].render(False), pos - 1 + + +class Heading: + + def __init__(self, text): + self.text = text + + def render(self, focus): + opt = urwid.Text("\n" + self.text, align="left") + opt = urwid.AttrWrap(opt, "title") + opt = urwid.Padding(opt, align = "center", width = 40) + return opt + + +_neg = lambda: False + + +class Option: + + def __init__(self, text, shortcut, getstate=None, activate=None): + self.text = text + self.shortcut = shortcut + self.getstate = getstate or _neg + self.activate = activate or _neg + + def render(self, focus): + return _OptionWidget( + self, + self.text, + self.shortcut, + self.getstate(), + focus) + + +class Select(urwid.ListBox): + + def __init__(self, options): + self.walker = OptionWalker(options) + urwid.ListBox.__init__( + self, + self.walker + ) + self.options = options + self.keymap = {} + for i in options: + if hasattr(i, "shortcut") and i.shortcut: + if i.shortcut in self.keymap: + raise ValueError("Duplicate shortcut key: %s" % i.shortcut) + self.keymap[i.shortcut] = i + + def keypress(self, size, key): + if key == "enter" or key == " ": + self.get_focus()[0].option.activate() + return None + key = common.shortcuts(key) + if key in self.keymap: + self.keymap[key].activate() + self.set_focus(self.options.index(self.keymap[key])) + return None + return super(self.__class__, self).keypress(size, key) diff --git a/mitmproxy/console/signals.py b/mitmproxy/console/signals.py new file mode 100644 index 00000000..6a439bf3 --- /dev/null +++ b/mitmproxy/console/signals.py @@ -0,0 +1,43 @@ +import blinker + +# Show a status message in the action bar +sig_add_event = blinker.Signal() + + +def add_event(e, level): + sig_add_event.send( + None, + e=e, + level=level + ) + +# Show a status message in the action bar +status_message = blinker.Signal() + +# Prompt for input +status_prompt = blinker.Signal() + +# Prompt for a path +status_prompt_path = blinker.Signal() + +# Prompt for a single keystroke +status_prompt_onekey = blinker.Signal() + +# Call a callback in N seconds +call_in = blinker.Signal() + +# Focus the body, footer or header of the main window +focus = blinker.Signal() + +# Fired when settings change +update_settings = blinker.Signal() + +# Fired when a flow changes +flow_change = blinker.Signal() + +# Fired when the flow list or focus changes +flowlist_change = blinker.Signal() + +# Pop and push view state onto a stack +pop_view_state = blinker.Signal() +push_view_state = blinker.Signal() diff --git a/mitmproxy/console/statusbar.py b/mitmproxy/console/statusbar.py new file mode 100644 index 00000000..4cc63a54 --- /dev/null +++ b/mitmproxy/console/statusbar.py @@ -0,0 +1,258 @@ +import os.path + +import urwid + +import netlib.utils +from . import pathedit, signals, common + + +class ActionBar(urwid.WidgetWrap): + + def __init__(self): + urwid.WidgetWrap.__init__(self, None) + self.clear() + signals.status_message.connect(self.sig_message) + signals.status_prompt.connect(self.sig_prompt) + signals.status_prompt_path.connect(self.sig_path_prompt) + signals.status_prompt_onekey.connect(self.sig_prompt_onekey) + + self.last_path = "" + + self.prompting = False + self.onekey = False + self.pathprompt = False + + def sig_message(self, sender, message, expire=None): + w = urwid.Text(message) + self._w = w + self.prompting = False + if expire: + def cb(*args): + if w == self._w: + self.clear() + signals.call_in.send(seconds=expire, callback=cb) + + def prep_prompt(self, p): + return p.strip() + ": " + + def sig_prompt(self, sender, prompt, text, callback, args=()): + signals.focus.send(self, section="footer") + self._w = urwid.Edit(self.prep_prompt(prompt), text or "") + self.prompting = (callback, args) + + def sig_path_prompt(self, sender, prompt, callback, args=()): + signals.focus.send(self, section="footer") + self._w = pathedit.PathEdit( + self.prep_prompt(prompt), + os.path.dirname(self.last_path) + ) + self.pathprompt = True + self.prompting = (callback, args) + + def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): + """ + Keys are a set of (word, key) tuples. The appropriate key in the + word is highlighted. + """ + signals.focus.send(self, section="footer") + prompt = [prompt, " ("] + mkup = [] + for i, e in enumerate(keys): + mkup.extend(common.highlight_key(e[0], e[1])) + if i < len(keys) - 1: + mkup.append(",") + prompt.extend(mkup) + prompt.append(")? ") + self.onekey = set(i[1] for i in keys) + self._w = urwid.Edit(prompt, "") + self.prompting = (callback, args) + + def selectable(self): + return True + + def keypress(self, size, k): + if self.prompting: + if k == "esc": + self.prompt_done() + elif self.onekey: + if k == "enter": + self.prompt_done() + elif k in self.onekey: + self.prompt_execute(k) + elif k == "enter": + self.prompt_execute(self._w.get_edit_text()) + else: + if common.is_keypress(k): + self._w.keypress(size, k) + else: + return k + + def clear(self): + self._w = urwid.Text("") + self.prompting = False + + def prompt_done(self): + self.prompting = False + self.onekey = False + self.pathprompt = False + signals.status_message.send(message="") + signals.focus.send(self, section="body") + + def prompt_execute(self, txt): + if self.pathprompt: + self.last_path = txt + p, args = self.prompting + self.prompt_done() + msg = p(txt, *args) + if msg: + signals.status_message.send(message=msg, expire=1) + + +class StatusBar(urwid.WidgetWrap): + + def __init__(self, master, helptext): + self.master, self.helptext = master, helptext + self.ab = ActionBar() + self.ib = urwid.WidgetWrap(urwid.Text("")) + self._w = urwid.Pile([self.ib, self.ab]) + signals.update_settings.connect(self.sig_update_settings) + signals.flowlist_change.connect(self.sig_update_settings) + self.redraw() + + def sig_update_settings(self, sender): + self.redraw() + + def keypress(self, *args, **kwargs): + return self.ab.keypress(*args, **kwargs) + + def get_status(self): + r = [] + + if self.master.setheaders.count(): + r.append("[") + r.append(("heading_key", "H")) + r.append("eaders]") + if self.master.replacehooks.count(): + r.append("[") + r.append(("heading_key", "R")) + r.append("eplacing]") + if self.master.client_playback: + r.append("[") + r.append(("heading_key", "cplayback")) + r.append(":%s to go]" % self.master.client_playback.count()) + if self.master.server_playback: + r.append("[") + r.append(("heading_key", "splayback")) + if self.master.nopop: + r.append(":%s in file]" % self.master.server_playback.count()) + else: + r.append(":%s to go]" % self.master.server_playback.count()) + if self.master.get_ignore_filter(): + r.append("[") + r.append(("heading_key", "I")) + r.append("gnore:%d]" % len(self.master.get_ignore_filter())) + if self.master.get_tcp_filter(): + r.append("[") + r.append(("heading_key", "T")) + r.append("CP:%d]" % len(self.master.get_tcp_filter())) + if self.master.state.intercept_txt: + r.append("[") + r.append(("heading_key", "i")) + r.append(":%s]" % self.master.state.intercept_txt) + if self.master.state.limit_txt: + r.append("[") + r.append(("heading_key", "l")) + r.append(":%s]" % self.master.state.limit_txt) + if self.master.stickycookie_txt: + r.append("[") + r.append(("heading_key", "t")) + r.append(":%s]" % self.master.stickycookie_txt) + if self.master.stickyauth_txt: + r.append("[") + r.append(("heading_key", "u")) + r.append(":%s]" % self.master.stickyauth_txt) + if self.master.state.default_body_view.name != "Auto": + r.append("[") + r.append(("heading_key", "M")) + r.append(":%s]" % self.master.state.default_body_view.name) + + opts = [] + if self.master.anticache: + opts.append("anticache") + if self.master.anticomp: + opts.append("anticomp") + if self.master.showhost: + opts.append("showhost") + if not self.master.refresh_server_playback: + opts.append("norefresh") + if self.master.killextra: + opts.append("killextra") + if self.master.server.config.no_upstream_cert: + opts.append("no-upstream-cert") + if self.master.state.follow_focus: + opts.append("following") + if self.master.stream_large_bodies: + opts.append( + "stream:%s" % netlib.utils.pretty_size( + self.master.stream_large_bodies.max_size + ) + ) + + if opts: + r.append("[%s]" % (":".join(opts))) + + if self.master.server.config.mode in ["reverse", "upstream"]: + dst = self.master.server.config.upstream_server + r.append("[dest:%s]" % netlib.utils.unparse_url( + dst.scheme, + dst.address.host, + dst.address.port + )) + if self.master.scripts: + r.append("[") + r.append(("heading_key", "s")) + r.append("cripts:%s]" % len(self.master.scripts)) + # r.append("[lt:%0.3f]"%self.master.looptime) + + if self.master.stream: + r.append("[W:%s]" % self.master.stream_path) + + return r + + def redraw(self): + fc = self.master.state.flow_count() + if self.master.state.focus is None: + offset = 0 + else: + offset = min(self.master.state.focus + 1, fc) + t = [ + ('heading', ("[%s/%s]" % (offset, fc)).ljust(9)) + ] + + if self.master.server.bound: + host = self.master.server.address.host + if host == "0.0.0.0": + host = "*" + boundaddr = "[%s:%s]" % (host, self.master.server.address.port) + else: + boundaddr = "" + t.extend(self.get_status()) + status = urwid.AttrWrap(urwid.Columns([ + urwid.Text(t), + urwid.Text( + [ + self.helptext, + boundaddr + ], + align="right" + ), + ]), "heading") + self.ib._w = status + + def update(self, text): + self.helptext = text + self.redraw() + self.master.loop.draw_screen() + + def selectable(self): + return True diff --git a/mitmproxy/console/tabs.py b/mitmproxy/console/tabs.py new file mode 100644 index 00000000..b5423038 --- /dev/null +++ b/mitmproxy/console/tabs.py @@ -0,0 +1,70 @@ +import urwid + + +class Tab(urwid.WidgetWrap): + + def __init__(self, offset, content, attr, onclick): + """ + onclick is called on click with the tab offset as argument + """ + p = urwid.Text(content, align="center") + p = urwid.Padding(p, align="center", width=("relative", 100)) + p = urwid.AttrWrap(p, attr) + urwid.WidgetWrap.__init__(self, p) + self.offset = offset + self.onclick = onclick + + def mouse_event(self, size, event, button, col, row, focus): + if event == "mouse press" and button == 1: + self.onclick(self.offset) + return True + + +class Tabs(urwid.WidgetWrap): + + def __init__(self, tabs, tab_offset=0): + urwid.WidgetWrap.__init__(self, "") + self.tab_offset = tab_offset + self.tabs = tabs + self.show() + + def change_tab(self, offset): + self.tab_offset = offset + self.show() + + def keypress(self, size, key): + n = len(self.tabs) + if key in ["tab", "l"]: + self.change_tab((self.tab_offset + 1) % n) + elif key == "h": + self.change_tab((self.tab_offset - 1) % n) + return self._w.keypress(size, key) + + def show(self): + headers = [] + for i in range(len(self.tabs)): + txt = self.tabs[i][0]() + if i == self.tab_offset: + headers.append( + Tab( + i, + txt, + "heading", + self.change_tab + ) + ) + else: + headers.append( + Tab( + i, + txt, + "heading_inactive", + self.change_tab + ) + ) + headers = urwid.Columns(headers, dividechars=1) + self._w = urwid.Frame( + body = self.tabs[self.tab_offset][1](), + header = headers + ) + self._w.set_focus("body") diff --git a/mitmproxy/console/window.py b/mitmproxy/console/window.py new file mode 100644 index 00000000..47c284e4 --- /dev/null +++ b/mitmproxy/console/window.py @@ -0,0 +1,90 @@ +import urwid +from . import signals + + +class Window(urwid.Frame): + + def __init__(self, master, body, header, footer, helpctx): + urwid.Frame.__init__( + self, + urwid.AttrWrap(body, "background"), + header = urwid.AttrWrap(header, "background") if header else None, + footer = urwid.AttrWrap(footer, "background") if footer else None + ) + self.master = master + self.helpctx = helpctx + signals.focus.connect(self.sig_focus) + + def sig_focus(self, sender, section): + self.focus_position = section + + def mouse_event(self, *args, **kwargs): + # args: (size, event, button, col, row) + k = super(self.__class__, self).mouse_event(*args, **kwargs) + if not k: + if args[1] == "mouse drag": + signals.status_message.send( + message = "Hold down shift, alt or ctrl to select text.", + expire = 1 + ) + elif args[1] == "mouse press" and args[2] == 4: + self.keypress(args[0], "up") + elif args[1] == "mouse press" and args[2] == 5: + self.keypress(args[0], "down") + else: + return False + return True + + def keypress(self, size, k): + k = super(self.__class__, self).keypress(size, k) + if k == "?": + self.master.view_help(self.helpctx) + elif k == "c": + if not self.master.client_playback: + signals.status_prompt_path.send( + self, + prompt = "Client replay", + callback = self.master.client_playback_path + ) + else: + signals.status_prompt_onekey.send( + self, + prompt = "Stop current client replay?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.master.stop_client_playback_prompt, + ) + elif k == "i": + signals.status_prompt.send( + self, + prompt = "Intercept filter", + text = self.master.state.intercept_txt, + callback = self.master.set_intercept + ) + elif k == "o": + self.master.view_options() + elif k == "Q": + raise urwid.ExitMainLoop + elif k == "q": + signals.pop_view_state.send(self) + elif k == "S": + if not self.master.server_playback: + signals.status_prompt_path.send( + self, + prompt = "Server replay path", + callback = self.master.server_playback_path + ) + else: + signals.status_prompt_onekey.send( + self, + prompt = "Stop current server replay?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.master.stop_server_playback_prompt, + ) + else: + return k diff --git a/mitmproxy/contentviews.py b/mitmproxy/contentviews.py new file mode 100644 index 00000000..5335b59c --- /dev/null +++ b/mitmproxy/contentviews.py @@ -0,0 +1,622 @@ +""" +Mitmproxy Content Views +======================= + +mitmproxy includes a set of content views which can be used to format/decode/highlight data. +While they are currently used for HTTP message bodies only, the may be used in other contexts +in the future, e.g. to decode protobuf messages sent as WebSocket frames. + +Thus, the View API is very minimalistic. The only arguments are `data` and `**metadata`, +where `data` is the actual content (as bytes). The contents on metadata depend on the protocol in +use. For HTTP, the message headers are passed as the ``headers`` keyword argument. For HTTP +requests, the query parameters are passed as the ``query`` keyword argument. + +""" +from __future__ import (absolute_import, print_function, division) +import cStringIO +import json +import logging +import subprocess +import sys +import lxml.html +import lxml.etree +import datetime +from PIL import Image +from PIL.ExifTags import TAGS +import html2text +import six +from netlib.odict import ODict +from netlib import encoding +from netlib.utils import clean_bin, hexdump, urldecode, multipartdecode, parse_content_type +from . import utils +from .exceptions import ContentViewException +from .contrib import jsbeautifier +from .contrib.wbxml.ASCommandResponse import ASCommandResponse + +try: + import pyamf + from pyamf import remoting, flex +except ImportError: # pragma no cover + pyamf = None + +try: + import cssutils +except ImportError: # pragma no cover + cssutils = None +else: + cssutils.log.setLevel(logging.CRITICAL) + + cssutils.ser.prefs.keepComments = True + cssutils.ser.prefs.omitLastSemicolon = False + cssutils.ser.prefs.indentClosingBrace = False + cssutils.ser.prefs.validOnly = False + +# Default view cutoff *in lines* +VIEW_CUTOFF = 512 + +KEY_MAX = 30 + + +def format_dict(d): + """ + Helper function that transforms the given dictionary into a list of + ("key", key ) + ("value", value) + tuples, where key is padded to a uniform width. + """ + max_key_len = max(len(k) for k in d.keys()) + max_key_len = min(max_key_len, KEY_MAX) + for key, value in d.items(): + key += ":" + key = key.ljust(max_key_len + 2) + yield [ + ("header", key), + ("text", value) + ] + + +def format_text(text): + """ + Helper function that transforms bytes into the view output format. + """ + for line in text.splitlines(): + yield [("text", line)] + + +class View(object): + name = None + prompt = () + content_types = [] + + def __call__(self, data, **metadata): + """ + Transform raw data into human-readable output. + + Args: + data: the data to decode/format as bytes. + metadata: optional keyword-only arguments for metadata. Implementations must not + rely on a given argument being present. + + Returns: + A (description, content generator) tuple. + + The content generator yields lists of (style, text) tuples, where each list represents + a single line. ``text`` is a unfiltered byte string which may need to be escaped, + depending on the used output. + + Caveats: + The content generator must not yield tuples of tuples, + because urwid cannot process that. You have to yield a *list* of tuples per line. + """ + raise NotImplementedError() + + +class ViewAuto(View): + name = "Auto" + prompt = ("auto", "a") + content_types = [] + + def __call__(self, data, **metadata): + headers = metadata.get("headers", {}) + ctype = headers.get("content-type") + if data and ctype: + ct = parse_content_type(ctype) if ctype else None + ct = "%s/%s" % (ct[0], ct[1]) + if ct in content_types_map: + return content_types_map[ct][0](data, **metadata) + elif utils.isXML(data): + return get("XML")(data, **metadata) + if metadata.get("query"): + return get("Query")(data, **metadata) + if data and utils.isMostlyBin(data): + return get("Hex")(data) + if not data: + return "No content", [] + return get("Raw")(data) + + +class ViewRaw(View): + name = "Raw" + prompt = ("raw", "r") + content_types = [] + + def __call__(self, data, **metadata): + return "Raw", format_text(data) + + +class ViewHex(View): + name = "Hex" + prompt = ("hex", "e") + content_types = [] + + @staticmethod + def _format(data): + for offset, hexa, s in hexdump(data): + yield [ + ("offset", offset + " "), + ("text", hexa + " "), + ("text", s) + ] + + def __call__(self, data, **metadata): + return "Hex", self._format(data) + + +class ViewXML(View): + name = "XML" + prompt = ("xml", "x") + content_types = ["text/xml"] + + def __call__(self, data, **metadata): + parser = lxml.etree.XMLParser( + remove_blank_text=True, + resolve_entities=False, + strip_cdata=False, + recover=False + ) + try: + document = lxml.etree.fromstring(data, parser) + except lxml.etree.XMLSyntaxError: + return None + docinfo = document.getroottree().docinfo + + prev = [] + p = document.getroottree().getroot().getprevious() + while p is not None: + prev.insert( + 0, + lxml.etree.tostring(p) + ) + p = p.getprevious() + doctype = docinfo.doctype + if prev: + doctype += "\n".join(prev).strip() + doctype = doctype.strip() + + s = lxml.etree.tostring( + document, + pretty_print=True, + xml_declaration=True, + doctype=doctype or None, + encoding=docinfo.encoding + ) + + return "XML-like data", format_text(s) + + +class ViewJSON(View): + name = "JSON" + prompt = ("json", "s") + content_types = ["application/json"] + + def __call__(self, data, **metadata): + pretty_json = utils.pretty_json(data) + if pretty_json: + return "JSON", format_text(pretty_json) + + +class ViewHTML(View): + name = "HTML" + prompt = ("html", "h") + content_types = ["text/html"] + + def __call__(self, data, **metadata): + if utils.isXML(data): + parser = lxml.etree.HTMLParser( + strip_cdata=True, + remove_blank_text=True + ) + d = lxml.html.fromstring(data, parser=parser) + docinfo = d.getroottree().docinfo + s = lxml.etree.tostring( + d, + pretty_print=True, + doctype=docinfo.doctype, + encoding='utf8' + ) + return "HTML", format_text(s) + + +class ViewHTMLOutline(View): + name = "HTML Outline" + prompt = ("html outline", "o") + content_types = ["text/html"] + + def __call__(self, data, **metadata): + data = data.decode("utf-8") + h = html2text.HTML2Text(baseurl="") + h.ignore_images = True + h.body_width = 0 + outline = h.handle(data) + return "HTML Outline", format_text(outline) + + +class ViewURLEncoded(View): + name = "URL-encoded" + prompt = ("urlencoded", "u") + content_types = ["application/x-www-form-urlencoded"] + + def __call__(self, data, **metadata): + d = urldecode(data) + return "URLEncoded form", format_dict(ODict(d)) + + +class ViewMultipart(View): + name = "Multipart Form" + prompt = ("multipart", "m") + content_types = ["multipart/form-data"] + + @staticmethod + def _format(v): + yield [("highlight", "Form data:\n")] + for message in format_dict(ODict(v)): + yield message + + def __call__(self, data, **metadata): + headers = metadata.get("headers", {}) + v = multipartdecode(headers, data) + if v: + return "Multipart form", self._format(v) + + +if pyamf: + class DummyObject(dict): + + def __init__(self, alias): + dict.__init__(self) + + def __readamf__(self, input): + data = input.readObject() + self["data"] = data + + def pyamf_class_loader(s): + for i in pyamf.CLASS_LOADERS: + if i != pyamf_class_loader: + v = i(s) + if v: + return v + return DummyObject + + pyamf.register_class_loader(pyamf_class_loader) + + class ViewAMF(View): + name = "AMF" + prompt = ("amf", "f") + content_types = ["application/x-amf"] + + def unpack(self, b, seen=set([])): + if hasattr(b, "body"): + return self.unpack(b.body, seen) + if isinstance(b, DummyObject): + if id(b) in seen: + return "" + else: + seen.add(id(b)) + for k, v in b.items(): + b[k] = self.unpack(v, seen) + return b + elif isinstance(b, dict): + for k, v in b.items(): + b[k] = self.unpack(v, seen) + return b + elif isinstance(b, list): + return [self.unpack(i) for i in b] + elif isinstance(b, datetime.datetime): + return str(b) + elif isinstance(b, flex.ArrayCollection): + return [self.unpack(i, seen) for i in b] + else: + return b + + def _format(self, envelope): + for target, message in iter(envelope): + if isinstance(message, pyamf.remoting.Request): + yield [ + ("header", "Request: "), + ("text", str(target)), + ] + else: + yield [ + ("header", "Response: "), + ("text", "%s, code %s" % (target, message.status)), + ] + + s = json.dumps(self.unpack(message), indent=4) + for msg in format_text(s): + yield msg + + def __call__(self, data, **metadata): + envelope = remoting.decode(data, strict=False) + if envelope: + return "AMF v%s" % envelope.amfVersion, self._format(envelope) + + +class ViewJavaScript(View): + name = "JavaScript" + prompt = ("javascript", "j") + content_types = [ + "application/x-javascript", + "application/javascript", + "text/javascript" + ] + + def __call__(self, data, **metadata): + opts = jsbeautifier.default_options() + opts.indent_size = 2 + res = jsbeautifier.beautify(data, opts) + return "JavaScript", format_text(res) + + +class ViewCSS(View): + name = "CSS" + prompt = ("css", "c") + content_types = [ + "text/css" + ] + + def __call__(self, data, **metadata): + if cssutils: + sheet = cssutils.parseString(data) + beautified = sheet.cssText + else: + beautified = data + + return "CSS", format_text(beautified) + + +class ViewImage(View): + name = "Image" + prompt = ("image", "i") + content_types = [ + "image/png", + "image/jpeg", + "image/gif", + "image/vnd.microsoft.icon", + "image/x-icon", + ] + + def __call__(self, data, **metadata): + try: + img = Image.open(cStringIO.StringIO(data)) + except IOError: + return None + parts = [ + ("Format", str(img.format_description)), + ("Size", "%s x %s px" % img.size), + ("Mode", str(img.mode)), + ] + for i in sorted(img.info.keys()): + if i != "exif": + parts.append( + (str(i), str(img.info[i])) + ) + if hasattr(img, "_getexif"): + ex = img._getexif() + if ex: + for i in sorted(ex.keys()): + tag = TAGS.get(i, i) + parts.append( + (str(tag), str(ex[i])) + ) + fmt = format_dict(ODict(parts)) + return "%s image" % img.format, fmt + + +class ViewProtobuf(View): + + """Human friendly view of protocol buffers + The view uses the protoc compiler to decode the binary + """ + + name = "Protocol Buffer" + prompt = ("protobuf", "p") + content_types = [ + "application/x-protobuf", + "application/x-protobuffer", + ] + + @staticmethod + def is_available(): + try: + p = subprocess.Popen( + ["protoc", "--version"], + stdout=subprocess.PIPE + ) + out, _ = p.communicate() + return out.startswith("libprotoc") + except: + return False + + def decode_protobuf(self, content): + # if Popen raises OSError, it will be caught in + # get_content_view and fall back to Raw + p = subprocess.Popen(['protoc', '--decode_raw'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = p.communicate(input=content) + if out: + return out + else: + return err + + def __call__(self, data, **metadata): + decoded = self.decode_protobuf(data) + return "Protobuf", format_text(decoded) + + +class ViewQuery(View): + name = "Query" + prompt = ("query", "q") + content_types = [] + + def __call__(self, data, **metadata): + query = metadata.get("query") + if query: + return "Query", format_dict(query) + else: + return "Query", format_text("") + + +class ViewWBXML(View): + name = "WBXML" + prompt = ("wbxml", "w") + content_types = [ + "application/vnd.wap.wbxml", + "application/vnd.ms-sync.wbxml" + ] + + def __call__(self, data, **metadata): + + try: + parser = ASCommandResponse(data) + parsedContent = parser.xmlString + if parsedContent: + return "WBXML", format_text(parsedContent) + except: + return None + + +views = [] +content_types_map = {} +view_prompts = [] + + +def get(name): + for i in views: + if i.name == name: + return i + + +def get_by_shortcut(c): + for i in views: + if i.prompt[1] == c: + return i + + +def add(view): + # TODO: auto-select a different name (append an integer?) + for i in views: + if i.name == view.name: + raise ContentViewException("Duplicate view: " + view.name) + + # TODO: the UI should auto-prompt for a replacement shortcut + for prompt in view_prompts: + if prompt[1] == view.prompt[1]: + raise ContentViewException("Duplicate view shortcut: " + view.prompt[1]) + + views.append(view) + + for ct in view.content_types: + l = content_types_map.setdefault(ct, []) + l.append(view) + + view_prompts.append(view.prompt) + + +def remove(view): + for ct in view.content_types: + l = content_types_map.setdefault(ct, []) + l.remove(view) + + if not len(l): + del content_types_map[ct] + + view_prompts.remove(view.prompt) + views.remove(view) + + +add(ViewAuto()) +add(ViewRaw()) +add(ViewHex()) +add(ViewJSON()) +add(ViewXML()) +add(ViewWBXML()) +add(ViewHTML()) +add(ViewHTMLOutline()) +add(ViewJavaScript()) +add(ViewCSS()) +add(ViewURLEncoded()) +add(ViewMultipart()) +add(ViewImage()) +add(ViewQuery()) + +if pyamf: + add(ViewAMF()) + +if ViewProtobuf.is_available(): + add(ViewProtobuf()) + + +def safe_to_print(lines, encoding="utf8"): + """ + Wraps a content generator so that each text portion is a *safe to print* unicode string. + """ + for line in lines: + clean_line = [] + for (style, text) in line: + try: + text = clean_bin(text.decode(encoding, "strict")) + except UnicodeDecodeError: + text = clean_bin(text).decode(encoding, "strict") + clean_line.append((style, text)) + yield clean_line + + +def get_content_view(viewmode, data, **metadata): + """ + Args: + viewmode: the view to use. + data, **metadata: arguments passed to View instance. + + Returns: + A (description, content generator) tuple. + In contrast to calling the views directly, text is always safe-to-print unicode. + + Raises: + ContentViewException, if the content view threw an error. + """ + msg = [] + + headers = metadata.get("headers", {}) + enc = headers.get("content-encoding") + if enc and enc != "identity": + decoded = encoding.decode(enc, data) + if decoded: + data = decoded + msg.append("[decoded %s]" % enc) + try: + ret = viewmode(data, **metadata) + # Third-party viewers can fail in unexpected ways... + except Exception as e: + six.reraise( + ContentViewException, + ContentViewException(str(e)), + sys.exc_info()[2] + ) + if not ret: + ret = get("Raw")(data, **metadata) + msg.append("Couldn't parse: falling back to Raw") + else: + msg.append(ret[0]) + return " ".join(msg), safe_to_print(ret[1]) diff --git a/mitmproxy/contrib/README b/mitmproxy/contrib/README new file mode 100644 index 00000000..e5ce11da --- /dev/null +++ b/mitmproxy/contrib/README @@ -0,0 +1,14 @@ + +Contribs: + +jsbeautifier, git checkout 25/03/12, MIT license + - Removed test directories + - Disabled packers through a single-line modification (see "# CORTESI" + comment) + +wbxml + - https://github.com/davidpshaw/PyWBXMLDecoder + +tls, BSD license + - https://github.com/mhils/tls/tree/mitmproxy + - limited to required files. \ No newline at end of file diff --git a/mitmproxy/contrib/__init__.py b/mitmproxy/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mitmproxy/contrib/jsbeautifier/__init__.py b/mitmproxy/contrib/jsbeautifier/__init__.py new file mode 100644 index 00000000..e319e8dd --- /dev/null +++ b/mitmproxy/contrib/jsbeautifier/__init__.py @@ -0,0 +1,1153 @@ +import sys +import getopt +import re +import string + +# +# Originally written by Einar Lielmanis et al., +# Conversion to python by Einar Lielmanis, einar@jsbeautifier.org, +# MIT licence, enjoy. +# +# Python is not my native language, feel free to push things around. +# +# Use either from command line (script displays its usage when run +# without any parameters), +# +# +# or, alternatively, use it as a module: +# +# import jsbeautifier +# res = jsbeautifier.beautify('your javascript string') +# res = jsbeautifier.beautify_file('some_file.js') +# +# you may specify some options: +# +# opts = jsbeautifier.default_options() +# opts.indent_size = 2 +# res = jsbeautifier.beautify('some javascript', opts) +# +# +# Here are the available options: (read source) + + +class BeautifierOptions: + def __init__(self): + self.indent_size = 4 + self.indent_char = ' ' + self.indent_with_tabs = False + self.preserve_newlines = True + self.max_preserve_newlines = 10. + self.jslint_happy = False + self.brace_style = 'collapse' + self.keep_array_indentation = False + self.keep_function_indentation = False + self.eval_code = False + + + + def __repr__(self): + return \ +"""indent_size = %d +indent_char = [%s] +preserve_newlines = %s +max_preserve_newlines = %d +jslint_happy = %s +indent_with_tabs = %s +brace_style = %s +keep_array_indentation = %s +eval_code = %s +""" % ( self.indent_size, + self.indent_char, + self.preserve_newlines, + self.max_preserve_newlines, + self.jslint_happy, + self.indent_with_tabs, + self.brace_style, + self.keep_array_indentation, + self.eval_code, + ) + + +class BeautifierFlags: + def __init__(self, mode): + self.previous_mode = 'BLOCK' + self.mode = mode + self.var_line = False + self.var_line_tainted = False + self.var_line_reindented = False + self.in_html_comment = False + self.if_line = False + self.in_case = False + self.eat_next_space = False + self.indentation_baseline = -1 + self.indentation_level = 0 + self.ternary_depth = 0 + + +def default_options(): + return BeautifierOptions() + + +def beautify(string, opts = default_options() ): + b = Beautifier() + return b.beautify(string, opts) + +def beautify_file(file_name, opts = default_options() ): + + if file_name == '-': # stdin + f = sys.stdin + else: + try: + f = open(file_name) + except Exception as ex: + return 'The file could not be opened' + + b = Beautifier() + return b.beautify(''.join(f.readlines()), opts) + + +def usage(): + + print("""Javascript beautifier (http://jsbeautifier.org/) + +Usage: jsbeautifier.py [options] + + can be "-", which means stdin. + defaults to stdout + +Input options: + + -i, --stdin read input from stdin + +Output options: + + -s, --indent-size=NUMBER indentation size. (default 4). + -c, --indent-char=CHAR character to indent with. (default space). + -t, --indent-with-tabs Indent with tabs, overrides -s and -c + -d, --disable-preserve-newlines do not preserve existing line breaks. + -j, --jslint-happy more jslint-compatible output + -b, --brace-style=collapse brace style (collapse, expand, end-expand) + -k, --keep-array-indentation keep array indentation. + -o, --outfile=FILE specify a file to output to (default stdout) + -f, --keep-function-indentation Do not re-indent function bodies defined in var lines. + +Rarely needed options: + + --eval-code evaluate code if a JS interpreter is + installed. May be useful with some obfuscated + script but poses a potential security issue. + + -l, --indent-level=NUMBER initial indentation level. (default 0). + + -h, --help, --usage prints this help statement. + +""") + + + + + + +class Beautifier: + + def __init__(self, opts = default_options() ): + + self.opts = opts + self.blank_state() + + def blank_state(self): + + # internal flags + self.flags = BeautifierFlags('BLOCK') + self.flag_store = [] + self.wanted_newline = False + self.just_added_newline = False + self.do_block_just_closed = False + + if self.opts.indent_with_tabs: + self.indent_string = "\t" + else: + self.indent_string = self.opts.indent_char * self.opts.indent_size + + self.preindent_string = '' + self.last_word = '' # last TK_WORD seen + self.last_type = 'TK_START_EXPR' # last token type + self.last_text = '' # last token text + self.last_last_text = '' # pre-last token text + + self.input = None + self.output = [] # formatted javascript gets built here + + self.whitespace = ["\n", "\r", "\t", " "] + self.wordchar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$' + self.digits = '0123456789' + self.punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |= ::' + self.punct += ' <%= <% %>' + self.punct = self.punct.split(' ') + + + # Words which always should start on a new line + self.line_starters = 'continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(',') + self.set_mode('BLOCK') + + global parser_pos + parser_pos = 0 + + + def beautify(self, s, opts = None ): + + if opts != None: + self.opts = opts + + + if self.opts.brace_style not in ['expand', 'collapse', 'end-expand']: + raise(Exception('opts.brace_style must be "expand", "collapse" or "end-expand".')) + + self.blank_state() + + while s and s[0] in [' ', '\t']: + self.preindent_string += s[0] + s = s[1:] + + #self.input = self.unpack(s, opts.eval_code) + # CORTESI + self.input = s + + parser_pos = 0 + while True: + token_text, token_type = self.get_next_token() + #print (token_text, token_type, self.flags.mode) + if token_type == 'TK_EOF': + break + + handlers = { + 'TK_START_EXPR': self.handle_start_expr, + 'TK_END_EXPR': self.handle_end_expr, + 'TK_START_BLOCK': self.handle_start_block, + 'TK_END_BLOCK': self.handle_end_block, + 'TK_WORD': self.handle_word, + 'TK_SEMICOLON': self.handle_semicolon, + 'TK_STRING': self.handle_string, + 'TK_EQUALS': self.handle_equals, + 'TK_OPERATOR': self.handle_operator, + 'TK_BLOCK_COMMENT': self.handle_block_comment, + 'TK_INLINE_COMMENT': self.handle_inline_comment, + 'TK_COMMENT': self.handle_comment, + 'TK_UNKNOWN': self.handle_unknown, + } + + handlers[token_type](token_text) + + self.last_last_text = self.last_text + self.last_type = token_type + self.last_text = token_text + + sweet_code = self.preindent_string + re.sub('[\n ]+$', '', ''.join(self.output)) + return sweet_code + + def unpack(self, source, evalcode=False): + import jsbeautifier.unpackers as unpackers + try: + return unpackers.run(source, evalcode) + except unpackers.UnpackingError as error: + print('error:', error) + return '' + + def trim_output(self, eat_newlines = False): + while len(self.output) \ + and ( + self.output[-1] == ' '\ + or self.output[-1] == self.indent_string \ + or self.output[-1] == self.preindent_string \ + or (eat_newlines and self.output[-1] in ['\n', '\r'])): + self.output.pop() + + def is_special_word(self, s): + return s in ['case', 'return', 'do', 'if', 'throw', 'else']; + + def is_array(self, mode): + return mode in ['[EXPRESSION]', '[INDENDED-EXPRESSION]'] + + + def is_expression(self, mode): + return mode in ['[EXPRESSION]', '[INDENDED-EXPRESSION]', '(EXPRESSION)', '(FOR-EXPRESSION)', '(COND-EXPRESSION)'] + + + def append_newline_forced(self): + old_array_indentation = self.opts.keep_array_indentation + self.opts.keep_array_indentation = False + self.append_newline() + self.opts.keep_array_indentation = old_array_indentation + + def append_newline(self, ignore_repeated = True): + + self.flags.eat_next_space = False + + if self.opts.keep_array_indentation and self.is_array(self.flags.mode): + return + + self.flags.if_line = False + self.trim_output() + + if len(self.output) == 0: + # no newline on start of file + return + + if self.output[-1] != '\n' or not ignore_repeated: + self.just_added_newline = True + self.output.append('\n') + + if self.preindent_string: + self.output.append(self.preindent_string) + + for i in range(self.flags.indentation_level): + self.output.append(self.indent_string) + + if self.flags.var_line and self.flags.var_line_reindented: + self.output.append(self.indent_string) + + + def append(self, s): + if s == ' ': + # do not add just a single space after the // comment, ever + if self.last_type == 'TK_COMMENT': + return self.append_newline() + + # make sure only single space gets drawn + if self.flags.eat_next_space: + self.flags.eat_next_space = False + elif len(self.output) and self.output[-1] not in [' ', '\n', self.indent_string]: + self.output.append(' ') + else: + self.just_added_newline = False + self.flags.eat_next_space = False + self.output.append(s) + + + def indent(self): + self.flags.indentation_level = self.flags.indentation_level + 1 + + + def remove_indent(self): + if len(self.output) and self.output[-1] in [self.indent_string, self.preindent_string]: + self.output.pop() + + + def set_mode(self, mode): + + prev = BeautifierFlags('BLOCK') + + if self.flags: + self.flag_store.append(self.flags) + prev = self.flags + + self.flags = BeautifierFlags(mode) + + if len(self.flag_store) == 1: + self.flags.indentation_level = 0 + else: + self.flags.indentation_level = prev.indentation_level + if prev.var_line and prev.var_line_reindented: + self.flags.indentation_level = self.flags.indentation_level + 1 + self.flags.previous_mode = prev.mode + + + def restore_mode(self): + self.do_block_just_closed = self.flags.mode == 'DO_BLOCK' + if len(self.flag_store) > 0: + mode = self.flags.mode + self.flags = self.flag_store.pop() + self.flags.previous_mode = mode + + + def get_next_token(self): + + global parser_pos + + self.n_newlines = 0 + + if parser_pos >= len(self.input): + return '', 'TK_EOF' + + self.wanted_newline = False + c = self.input[parser_pos] + parser_pos += 1 + + keep_whitespace = self.opts.keep_array_indentation and self.is_array(self.flags.mode) + + if keep_whitespace: + # slight mess to allow nice preservation of array indentation and reindent that correctly + # first time when we get to the arrays: + # var a = [ + # ....'something' + # we make note of whitespace_count = 4 into flags.indentation_baseline + # so we know that 4 whitespaces in original source match indent_level of reindented source + # + # and afterwards, when we get to + # 'something, + # .......'something else' + # we know that this should be indented to indent_level + (7 - indentation_baseline) spaces + + whitespace_count = 0 + while c in self.whitespace: + if c == '\n': + self.trim_output() + self.output.append('\n') + self.just_added_newline = True + whitespace_count = 0 + elif c == '\t': + whitespace_count += 4 + elif c == '\r': + pass + else: + whitespace_count += 1 + + if parser_pos >= len(self.input): + return '', 'TK_EOF' + + c = self.input[parser_pos] + parser_pos += 1 + + if self.flags.indentation_baseline == -1: + + self.flags.indentation_baseline = whitespace_count + + if self.just_added_newline: + for i in range(self.flags.indentation_level + 1): + self.output.append(self.indent_string) + + if self.flags.indentation_baseline != -1: + for i in range(whitespace_count - self.flags.indentation_baseline): + self.output.append(' ') + + else: # not keep_whitespace + while c in self.whitespace: + if c == '\n': + if self.opts.max_preserve_newlines == 0 or self.opts.max_preserve_newlines > self.n_newlines: + self.n_newlines += 1 + + if parser_pos >= len(self.input): + return '', 'TK_EOF' + + c = self.input[parser_pos] + parser_pos += 1 + + if self.opts.preserve_newlines and self.n_newlines > 1: + for i in range(self.n_newlines): + self.append_newline(i == 0) + self.just_added_newline = True + + self.wanted_newline = self.n_newlines > 0 + + + if c in self.wordchar: + if parser_pos < len(self.input): + while self.input[parser_pos] in self.wordchar: + c = c + self.input[parser_pos] + parser_pos += 1 + if parser_pos == len(self.input): + break + + # small and surprisingly unugly hack for 1E-10 representation + if parser_pos != len(self.input) and self.input[parser_pos] in '+-' \ + and re.match('^[0-9]+[Ee]$', c): + + sign = self.input[parser_pos] + parser_pos += 1 + t = self.get_next_token() + c += sign + t[0] + return c, 'TK_WORD' + + if c == 'in': # in is an operator, need to hack + return c, 'TK_OPERATOR' + + if self.wanted_newline and \ + self.last_type != 'TK_OPERATOR' and\ + self.last_type != 'TK_EQUALS' and\ + not self.flags.if_line and \ + (self.opts.preserve_newlines or self.last_text != 'var'): + self.append_newline() + + return c, 'TK_WORD' + + if c in '([': + return c, 'TK_START_EXPR' + + if c in ')]': + return c, 'TK_END_EXPR' + + if c == '{': + return c, 'TK_START_BLOCK' + + if c == '}': + return c, 'TK_END_BLOCK' + + if c == ';': + return c, 'TK_SEMICOLON' + + if c == '/': + comment = '' + inline_comment = True + comment_mode = 'TK_INLINE_COMMENT' + if self.input[parser_pos] == '*': # peek /* .. */ comment + parser_pos += 1 + if parser_pos < len(self.input): + while not (self.input[parser_pos] == '*' and \ + parser_pos + 1 < len(self.input) and \ + self.input[parser_pos + 1] == '/')\ + and parser_pos < len(self.input): + c = self.input[parser_pos] + comment += c + if c in '\r\n': + comment_mode = 'TK_BLOCK_COMMENT' + parser_pos += 1 + if parser_pos >= len(self.input): + break + parser_pos += 2 + return '/*' + comment + '*/', comment_mode + if self.input[parser_pos] == '/': # peek // comment + comment = c + while self.input[parser_pos] not in '\r\n': + comment += self.input[parser_pos] + parser_pos += 1 + if parser_pos >= len(self.input): + break + parser_pos += 1 + if self.wanted_newline: + self.append_newline() + return comment, 'TK_COMMENT' + + + + if c == "'" or c == '"' or \ + (c == '/' and ((self.last_type == 'TK_WORD' and self.is_special_word(self.last_text)) or \ + (self.last_type == 'TK_END_EXPR' and self.flags.previous_mode in ['(FOR-EXPRESSION)', '(COND-EXPRESSION)']) or \ + (self.last_type in ['TK_COMMENT', 'TK_START_EXPR', 'TK_START_BLOCK', 'TK_END_BLOCK', 'TK_OPERATOR', + 'TK_EQUALS', 'TK_EOF', 'TK_SEMICOLON']))): + sep = c + esc = False + resulting_string = c + in_char_class = False + + if parser_pos < len(self.input): + if sep == '/': + # handle regexp + in_char_class = False + while esc or in_char_class or self.input[parser_pos] != sep: + resulting_string += self.input[parser_pos] + if not esc: + esc = self.input[parser_pos] == '\\' + if self.input[parser_pos] == '[': + in_char_class = True + elif self.input[parser_pos] == ']': + in_char_class = False + else: + esc = False + parser_pos += 1 + if parser_pos >= len(self.input): + # incomplete regex when end-of-file reached + # bail out with what has received so far + return resulting_string, 'TK_STRING' + else: + # handle string + while esc or self.input[parser_pos] != sep: + resulting_string += self.input[parser_pos] + if not esc: + esc = self.input[parser_pos] == '\\' + else: + esc = False + parser_pos += 1 + if parser_pos >= len(self.input): + # incomplete string when end-of-file reached + # bail out with what has received so far + return resulting_string, 'TK_STRING' + + + parser_pos += 1 + resulting_string += sep + if sep == '/': + # regexps may have modifiers /regexp/MOD, so fetch those too + while parser_pos < len(self.input) and self.input[parser_pos] in self.wordchar: + resulting_string += self.input[parser_pos] + parser_pos += 1 + return resulting_string, 'TK_STRING' + + if c == '#': + + # she-bang + if len(self.output) == 0 and len(self.input) > 1 and self.input[parser_pos] == '!': + resulting_string = c + while parser_pos < len(self.input) and c != '\n': + c = self.input[parser_pos] + resulting_string += c + parser_pos += 1 + self.output.append(resulting_string.strip() + "\n") + self.append_newline() + return self.get_next_token() + + + # Spidermonkey-specific sharp variables for circular references + # https://developer.mozilla.org/En/Sharp_variables_in_JavaScript + # http://mxr.mozilla.org/mozilla-central/source/js/src/jsscan.cpp around line 1935 + sharp = '#' + if parser_pos < len(self.input) and self.input[parser_pos] in self.digits: + while True: + c = self.input[parser_pos] + sharp += c + parser_pos += 1 + if parser_pos >= len(self.input) or c == '#' or c == '=': + break + if c == '#' or parser_pos >= len(self.input): + pass + elif self.input[parser_pos] == '[' and self.input[parser_pos + 1] == ']': + sharp += '[]' + parser_pos += 2 + elif self.input[parser_pos] == '{' and self.input[parser_pos + 1] == '}': + sharp += '{}' + parser_pos += 2 + return sharp, 'TK_WORD' + + if c == '<' and self.input[parser_pos - 1 : parser_pos + 3] == '': + self.flags.in_html_comment = False + parser_pos += 2 + if self.wanted_newline: + self.append_newline() + return '-->', 'TK_COMMENT' + + if c in self.punct: + while parser_pos < len(self.input) and c + self.input[parser_pos] in self.punct: + c += self.input[parser_pos] + parser_pos += 1 + if parser_pos >= len(self.input): + break + if c == '=': + return c, 'TK_EQUALS' + else: + return c, 'TK_OPERATOR' + return c, 'TK_UNKNOWN' + + + + def handle_start_expr(self, token_text): + if token_text == '[': + if self.last_type == 'TK_WORD' or self.last_text == ')': + if self.last_text in self.line_starters: + self.append(' ') + self.set_mode('(EXPRESSION)') + self.append(token_text) + return + + if self.flags.mode in ['[EXPRESSION]', '[INDENTED-EXPRESSION]']: + if self.last_last_text == ']' and self.last_text == ',': + # ], [ goes to a new line + if self.flags.mode == '[EXPRESSION]': + self.flags.mode = '[INDENTED-EXPRESSION]' + if not self.opts.keep_array_indentation: + self.indent() + self.set_mode('[EXPRESSION]') + if not self.opts.keep_array_indentation: + self.append_newline() + elif self.last_text == '[': + if self.flags.mode == '[EXPRESSION]': + self.flags.mode = '[INDENTED-EXPRESSION]' + if not self.opts.keep_array_indentation: + self.indent() + self.set_mode('[EXPRESSION]') + + if not self.opts.keep_array_indentation: + self.append_newline() + else: + self.set_mode('[EXPRESSION]') + else: + self.set_mode('[EXPRESSION]') + else: + if self.last_text == 'for': + self.set_mode('(FOR-EXPRESSION)') + elif self.last_text in ['if', 'while']: + self.set_mode('(COND-EXPRESSION)') + else: + self.set_mode('(EXPRESSION)') + + + if self.last_text == ';' or self.last_type == 'TK_START_BLOCK': + self.append_newline() + elif self.last_type in ['TK_END_EXPR', 'TK_START_EXPR', 'TK_END_BLOCK'] or self.last_text == '.': + # do nothing on (( and )( and ][ and ]( and .( + if self.wanted_newline: + self.append_newline(); + elif self.last_type not in ['TK_WORD', 'TK_OPERATOR']: + self.append(' ') + elif self.last_word == 'function' or self.last_word == 'typeof': + # function() vs function (), typeof() vs typeof () + if self.opts.jslint_happy: + self.append(' ') + elif self.last_text in self.line_starters or self.last_text == 'catch': + self.append(' ') + + self.append(token_text) + + + def handle_end_expr(self, token_text): + if token_text == ']': + if self.opts.keep_array_indentation: + if self.last_text == '}': + self.remove_indent() + self.append(token_text) + self.restore_mode() + return + else: + if self.flags.mode == '[INDENTED-EXPRESSION]': + if self.last_text == ']': + self.restore_mode() + self.append_newline() + self.append(token_text) + return + self.restore_mode() + self.append(token_text) + + + def handle_start_block(self, token_text): + if self.last_word == 'do': + self.set_mode('DO_BLOCK') + else: + self.set_mode('BLOCK') + + if self.opts.brace_style == 'expand': + if self.last_type != 'TK_OPERATOR': + if self.last_text == '=' or (self.is_special_word(self.last_text) and self.last_text != 'else'): + self.append(' ') + else: + self.append_newline(True) + + self.append(token_text) + self.indent() + else: + if self.last_type not in ['TK_OPERATOR', 'TK_START_EXPR']: + if self.last_type == 'TK_START_BLOCK': + self.append_newline() + else: + self.append(' ') + else: + # if TK_OPERATOR or TK_START_EXPR + if self.is_array(self.flags.previous_mode) and self.last_text == ',': + if self.last_last_text == '}': + self.append(' ') + else: + self.append_newline() + self.indent() + self.append(token_text) + + + def handle_end_block(self, token_text): + self.restore_mode() + if self.opts.brace_style == 'expand': + if self.last_text != '{': + self.append_newline() + else: + if self.last_type == 'TK_START_BLOCK': + if self.just_added_newline: + self.remove_indent() + else: + # {} + self.trim_output() + else: + if self.is_array(self.flags.mode) and self.opts.keep_array_indentation: + self.opts.keep_array_indentation = False + self.append_newline() + self.opts.keep_array_indentation = True + else: + self.append_newline() + + self.append(token_text) + + + def handle_word(self, token_text): + if self.do_block_just_closed: + self.append(' ') + self.append(token_text) + self.append(' ') + self.do_block_just_closed = False + return + + if token_text == 'function': + + if self.flags.var_line: + self.flags.var_line_reindented = not self.opts.keep_function_indentation + if (self.just_added_newline or self.last_text == ';') and self.last_text != '{': + # make sure there is a nice clean space of at least one blank line + # before a new function definition + have_newlines = self.n_newlines + if not self.just_added_newline: + have_newlines = 0 + if not self.opts.preserve_newlines: + have_newlines = 1 + for i in range(2 - have_newlines): + self.append_newline(False) + + if token_text in ['case', 'default']: + if self.last_text == ':': + self.remove_indent() + else: + self.flags.indentation_level -= 1 + self.append_newline() + self.flags.indentation_level += 1 + self.append(token_text) + self.flags.in_case = True + return + + prefix = 'NONE' + + if self.last_type == 'TK_END_BLOCK': + if token_text not in ['else', 'catch', 'finally']: + prefix = 'NEWLINE' + else: + if self.opts.brace_style in ['expand', 'end-expand']: + prefix = 'NEWLINE' + else: + prefix = 'SPACE' + self.append(' ') + elif self.last_type == 'TK_SEMICOLON' and self.flags.mode in ['BLOCK', 'DO_BLOCK']: + prefix = 'NEWLINE' + elif self.last_type == 'TK_SEMICOLON' and self.is_expression(self.flags.mode): + prefix = 'SPACE' + elif self.last_type == 'TK_STRING': + prefix = 'NEWLINE' + elif self.last_type == 'TK_WORD': + if self.last_text == 'else': + # eat newlines between ...else *** some_op... + # won't preserve extra newlines in this place (if any), but don't care that much + self.trim_output(True) + prefix = 'SPACE' + elif self.last_type == 'TK_START_BLOCK': + prefix = 'NEWLINE' + elif self.last_type == 'TK_END_EXPR': + self.append(' ') + prefix = 'NEWLINE' + + if self.flags.if_line and self.last_type == 'TK_END_EXPR': + self.flags.if_line = False + + if token_text in self.line_starters: + if self.last_text == 'else': + prefix = 'SPACE' + else: + prefix = 'NEWLINE' + + if token_text == 'function' and self.last_text in ['get', 'set']: + prefix = 'SPACE' + + if token_text in ['else', 'catch', 'finally']: + if self.last_type != 'TK_END_BLOCK' \ + or self.opts.brace_style == 'expand' \ + or self.opts.brace_style == 'end-expand': + self.append_newline() + else: + self.trim_output(True) + self.append(' ') + elif prefix == 'NEWLINE': + if token_text == 'function' and (self.last_type == 'TK_START_EXPR' or self.last_text in '=,'): + # no need to force newline on "function" - + # (function... + pass + elif token_text == 'function' and self.last_text == 'new': + self.append(' ') + elif self.is_special_word(self.last_text): + # no newline between return nnn + self.append(' ') + elif self.last_type != 'TK_END_EXPR': + if (self.last_type != 'TK_START_EXPR' or token_text != 'var') and self.last_text != ':': + # no need to force newline on VAR - + # for (var x = 0... + if token_text == 'if' and self.last_word == 'else' and self.last_text != '{': + self.append(' ') + else: + self.flags.var_line = False + self.flags.var_line_reindented = False + self.append_newline() + elif token_text in self.line_starters and self.last_text != ')': + self.flags.var_line = False + self.flags.var_line_reindented = False + self.append_newline() + elif self.is_array(self.flags.mode) and self.last_text == ',' and self.last_last_text == '}': + self.append_newline() # }, in lists get a newline + elif prefix == 'SPACE': + self.append(' ') + + + self.append(token_text) + self.last_word = token_text + + if token_text == 'var': + self.flags.var_line = True + self.flags.var_line_reindented = False + self.flags.var_line_tainted = False + + + if token_text == 'if': + self.flags.if_line = True + + if token_text == 'else': + self.flags.if_line = False + + + def handle_semicolon(self, token_text): + self.append(token_text) + self.flags.var_line = False + self.flags.var_line_reindented = False + if self.flags.mode == 'OBJECT': + # OBJECT mode is weird and doesn't get reset too well. + self.flags.mode = 'BLOCK' + + + def handle_string(self, token_text): + if self.last_type == 'TK_END_EXPR' and self.flags.previous_mode in ['(COND-EXPRESSION)', '(FOR-EXPRESSION)']: + self.append(' ') + if self.last_type in ['TK_STRING', 'TK_START_BLOCK', 'TK_END_BLOCK', 'TK_SEMICOLON']: + self.append_newline() + elif self.last_type == 'TK_WORD': + self.append(' ') + + # Try to replace readable \x-encoded characters with their equivalent, + # if it is possible (e.g. '\x41\x42\x43\x01' becomes 'ABC\x01'). + def unescape(match): + block, code = match.group(0, 1) + char = chr(int(code, 16)) + if block.count('\\') == 1 and char in string.printable: + return char + return block + + token_text = re.sub(r'\\{1,2}x([a-fA-F0-9]{2})', unescape, token_text) + + self.append(token_text) + + def handle_equals(self, token_text): + if self.flags.var_line: + # just got an '=' in a var-line, different line breaking rules will apply + self.flags.var_line_tainted = True + + self.append(' ') + self.append(token_text) + self.append(' ') + + + def handle_operator(self, token_text): + space_before = True + space_after = True + + if self.flags.var_line and token_text == ',' and self.is_expression(self.flags.mode): + # do not break on comma, for ( var a = 1, b = 2 + self.flags.var_line_tainted = False + + if self.flags.var_line and token_text == ',': + if self.flags.var_line_tainted: + self.append(token_text) + self.flags.var_line_reindented = True + self.flags.var_line_tainted = False + self.append_newline() + return + else: + self.flags.var_line_tainted = False + + if self.is_special_word(self.last_text): + # return had a special handling in TK_WORD + self.append(' ') + self.append(token_text) + return + + if token_text == ':' and self.flags.in_case: + self.append(token_text) + self.append_newline() + self.flags.in_case = False + return + + if token_text == '::': + # no spaces around the exotic namespacing syntax operator + self.append(token_text) + return + + if token_text == ',': + if self.flags.var_line: + if self.flags.var_line_tainted: + # This never happens, as it's handled previously, right? + self.append(token_text) + self.append_newline() + self.flags.var_line_tainted = False + else: + self.append(token_text) + self.append(' ') + elif self.last_type == 'TK_END_BLOCK' and self.flags.mode != '(EXPRESSION)': + self.append(token_text) + if self.flags.mode == 'OBJECT' and self.last_text == '}': + self.append_newline() + else: + self.append(' ') + else: + if self.flags.mode == 'OBJECT': + self.append(token_text) + self.append_newline() + else: + # EXPR or DO_BLOCK + self.append(token_text) + self.append(' ') + # comma handled + return + elif token_text in ['--', '++', '!'] \ + or (token_text in ['+', '-'] \ + and self.last_type in ['TK_START_BLOCK', 'TK_START_EXPR', 'TK_EQUALS', 'TK_OPERATOR']) \ + or self.last_text in self.line_starters: + + space_before = False + space_after = False + + if self.last_text == ';' and self.is_expression(self.flags.mode): + # for (;; ++i) + # ^^ + space_before = True + + if self.last_type == 'TK_WORD' and self.last_text in self.line_starters: + space_before = True + + if self.flags.mode == 'BLOCK' and self.last_text in ['{', ';']: + # { foo: --i } + # foo(): --bar + self.append_newline() + + elif token_text == '.': + # decimal digits or object.property + space_before = False + + elif token_text == ':': + if self.flags.ternary_depth == 0: + self.flags.mode = 'OBJECT' + space_before = False + else: + self.flags.ternary_depth -= 1 + elif token_text == '?': + self.flags.ternary_depth += 1 + + if space_before: + self.append(' ') + + self.append(token_text) + + if space_after: + self.append(' ') + + + + def handle_block_comment(self, token_text): + + lines = token_text.replace('\x0d', '').split('\x0a') + # all lines start with an asterisk? that's a proper box comment + if not any(l for l in lines[1:] if ( l.strip() == '' or (l.lstrip())[0] != '*')): + self.append_newline() + self.append(lines[0]) + for line in lines[1:]: + self.append_newline() + self.append(' ' + line.strip()) + else: + # simple block comment: leave intact + if len(lines) > 1: + # multiline comment starts on a new line + self.append_newline() + else: + # single line /* ... */ comment stays on the same line + self.append(' ') + for line in lines: + self.append(line) + self.append('\n') + self.append_newline() + + + def handle_inline_comment(self, token_text): + self.append(' ') + self.append(token_text) + if self.is_expression(self.flags.mode): + self.append(' ') + else: + self.append_newline_forced() + + + def handle_comment(self, token_text): + if self.wanted_newline: + self.append_newline() + else: + self.append(' ') + + self.append(token_text) + self.append_newline_forced() + + + def handle_unknown(self, token_text): + if self.last_text in ['return', 'throw']: + self.append(' ') + + self.append(token_text) + + + + + +def main(): + + argv = sys.argv[1:] + + try: + opts, args = getopt.getopt(argv, "s:c:o:djbkil:htf", ['indent-size=','indent-char=','outfile=', 'disable-preserve-newlines', + 'jslint-happy', 'brace-style=', + 'keep-array-indentation', 'indent-level=', 'help', + 'usage', 'stdin', 'eval-code', 'indent-with-tabs', 'keep-function-indentation']) + except getopt.GetoptError: + return usage() + + js_options = default_options() + + file = None + outfile = 'stdout' + if len(args) == 1: + file = args[0] + + for opt, arg in opts: + if opt in ('--keep-array-indentation', '-k'): + js_options.keep_array_indentation = True + if opt in ('--keep-function-indentation','-f'): + js_options.keep_function_indentation = True + elif opt in ('--outfile', '-o'): + outfile = arg + elif opt in ('--indent-size', '-s'): + js_options.indent_size = int(arg) + elif opt in ('--indent-char', '-c'): + js_options.indent_char = arg + elif opt in ('--indent-with-tabs', '-t'): + js_options.indent_with_tabs = True + elif opt in ('--disable-preserve_newlines', '-d'): + js_options.preserve_newlines = False + elif opt in ('--jslint-happy', '-j'): + js_options.jslint_happy = True + elif opt in ('--eval-code'): + js_options.eval_code = True + elif opt in ('--brace-style', '-b'): + js_options.brace_style = arg + elif opt in ('--stdin', '-i'): + file = '-' + elif opt in ('--help', '--usage', '-h'): + return usage() + + if not file: + return usage() + else: + if outfile == 'stdout': + print(beautify_file(file, js_options)) + else: + with open(outfile, 'w') as f: + f.write(beautify_file(file, js_options) + '\n') + diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd b/mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd new file mode 100644 index 00000000..e937b762 --- /dev/null +++ b/mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd @@ -0,0 +1,25 @@ +# UNPACKERS SPECIFICATIONS + +Nothing very difficult: an unpacker is a submodule placed in the directory +where this file was found. Each unpacker must define three symbols: + + * `PRIORITY` : integer number expressing the priority in applying this + unpacker. Lower number means higher priority. + Makes sense only if a source file has been packed with + more than one packer. + * `detect(source)` : returns `True` if source is packed, otherwise, `False`. + * `unpack(source)` : takes a `source` string and unpacks it. Must always return + valid JavaScript. That is to say, your code should look + like: + +``` +if detect(source): + return do_your_fancy_things_with(source) +else: + return source +``` + +*You can safely define any other symbol in your module, as it will be ignored.* + +`__init__` code will automatically load new unpackers, without any further step +to be accomplished. Simply drop it in this directory. diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/__init__.py b/mitmproxy/contrib/jsbeautifier/unpackers/__init__.py new file mode 100644 index 00000000..fcb5b07a --- /dev/null +++ b/mitmproxy/contrib/jsbeautifier/unpackers/__init__.py @@ -0,0 +1,66 @@ +# +# General code for JSBeautifier unpackers infrastructure. See README.specs +# written by Stefano Sanfilippo +# + +"""General code for JSBeautifier unpackers infrastructure.""" + +import pkgutil +import re +from . import evalbased + +# NOTE: AT THE MOMENT, IT IS DEACTIVATED FOR YOUR SECURITY: it runs js! +BLACKLIST = ['jsbeautifier.unpackers.evalbased'] + +class UnpackingError(Exception): + """Badly packed source or general error. Argument is a + meaningful description.""" + +def getunpackers(): + """Scans the unpackers dir, finds unpackers and add them to UNPACKERS list. + An unpacker will be loaded only if it is a valid python module (name must + adhere to naming conventions) and it is not blacklisted (i.e. inserted + into BLACKLIST.""" + path = __path__ + prefix = __name__ + '.' + unpackers = [] + interface = ['unpack', 'detect', 'PRIORITY'] + for _importer, modname, _ispkg in pkgutil.iter_modules(path, prefix): + if 'tests' not in modname and modname not in BLACKLIST: + try: + module = __import__(modname, fromlist=interface) + except ImportError: + raise UnpackingError('Bad unpacker: %s' % modname) + else: + unpackers.append(module) + + return sorted(unpackers, key = lambda mod: mod.PRIORITY) + +UNPACKERS = getunpackers() + +def run(source, evalcode=False): + """Runs the applicable unpackers and return unpacked source as a string.""" + for unpacker in [mod for mod in UNPACKERS if mod.detect(source)]: + source = unpacker.unpack(source) + if evalcode and evalbased.detect(source): + source = evalbased.unpack(source) + return source + +def filtercomments(source): + """NOT USED: strips trailing comments and put them at the top.""" + trailing_comments = [] + comment = True + + while comment: + if re.search(r'^\s*\/\*', source): + comment = source[0, source.index('*/') + 2] + elif re.search(r'^\s*\/\/', source): + comment = re.search(r'^\s*\/\/', source).group(0) + else: + comment = None + + if comment: + source = re.sub(r'^\s+', '', source[len(comment):]) + trailing_comments.append(comment) + + return '\n'.join(trailing_comments) + source diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py b/mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py new file mode 100644 index 00000000..b17d926e --- /dev/null +++ b/mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py @@ -0,0 +1,39 @@ +# +# Unpacker for eval() based packers, a part of javascript beautifier +# by Einar Lielmanis +# +# written by Stefano Sanfilippo +# +# usage: +# +# if detect(some_string): +# unpacked = unpack(some_string) +# + +"""Unpacker for eval() based packers: runs JS code and returns result. +Works only if a JS interpreter (e.g. Mozilla's Rhino) is installed and +properly set up on host.""" + +from subprocess import PIPE, Popen + +PRIORITY = 3 + +def detect(source): + """Detects if source is likely to be eval() packed.""" + return source.strip().lower().startswith('eval(function(') + +def unpack(source): + """Runs source and return resulting code.""" + return jseval('print %s;' % source[4:]) if detect(source) else source + +# In case of failure, we'll just return the original, without crashing on user. +def jseval(script): + """Run code in the JS interpreter and return output.""" + try: + interpreter = Popen(['js'], stdin=PIPE, stdout=PIPE) + except OSError: + return script + result, errors = interpreter.communicate(script) + if interpreter.poll() or errors: + return script + return result diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py b/mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py new file mode 100644 index 00000000..aa4344a3 --- /dev/null +++ b/mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py @@ -0,0 +1,58 @@ +# +# simple unpacker/deobfuscator for scripts messed up with +# javascriptobfuscator.com +# +# written by Einar Lielmanis +# rewritten in Python by Stefano Sanfilippo +# +# Will always return valid javascript: if `detect()` is false, `code` is +# returned, unmodified. +# +# usage: +# +# if javascriptobfuscator.detect(some_string): +# some_string = javascriptobfuscator.unpack(some_string) +# + +"""deobfuscator for scripts messed up with JavascriptObfuscator.com""" + +import re + +PRIORITY = 1 + +def smartsplit(code): + """Split `code` at " symbol, only if it is not escaped.""" + strings = [] + pos = 0 + while pos < len(code): + if code[pos] == '"': + word = '' # new word + pos += 1 + while pos < len(code): + if code[pos] == '"': + break + if code[pos] == '\\': + word += '\\' + pos += 1 + word += code[pos] + pos += 1 + strings.append('"%s"' % word) + pos += 1 + return strings + +def detect(code): + """Detects if `code` is JavascriptObfuscator.com packed.""" + # prefer `is not` idiom, so that a true boolean is returned + return (re.search(r'^var _0x[a-f0-9]+ ?\= ?\[', code) is not None) + +def unpack(code): + """Unpacks JavascriptObfuscator.com packed code.""" + if detect(code): + matches = re.search(r'var (_0x[a-f\d]+) ?\= ?\[(.*?)\];', code) + if matches: + variable = matches.group(1) + dictionary = smartsplit(matches.group(2)) + code = code[len(matches.group(0)):] + for key, value in enumerate(dictionary): + code = code.replace(r'%s[%s]' % (variable, key), value) + return code diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py b/mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py new file mode 100644 index 00000000..9893f95f --- /dev/null +++ b/mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py @@ -0,0 +1,86 @@ +# +# deobfuscator for scripts messed up with myobfuscate.com +# by Einar Lielmanis +# +# written by Stefano Sanfilippo +# +# usage: +# +# if detect(some_string): +# unpacked = unpack(some_string) +# + +# CAVEAT by Einar Lielmanis + +# +# You really don't want to obfuscate your scripts there: they're tracking +# your unpackings, your script gets turned into something like this, +# as of 2011-08-26: +# +# var _escape = 'your_script_escaped'; +# var _111 = document.createElement('script'); +# _111.src = 'http://api.www.myobfuscate.com/?getsrc=ok' + +# '&ref=' + encodeURIComponent(document.referrer) + +# '&url=' + encodeURIComponent(document.URL); +# var 000 = document.getElementsByTagName('head')[0]; +# 000.appendChild(_111); +# document.write(unescape(_escape)); +# + +"""Deobfuscator for scripts messed up with MyObfuscate.com""" + +import re +import base64 + +# Python 2 retrocompatibility +# pylint: disable=F0401 +# pylint: disable=E0611 +try: + from urllib import unquote +except ImportError: + from urllib.parse import unquote + +from . import UnpackingError + +PRIORITY = 1 + +CAVEAT = """// +// Unpacker warning: be careful when using myobfuscate.com for your projects: +// scripts obfuscated by the free online version call back home. +// + +""" + +SIGNATURE = (r'["\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F' + r'\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x61\x62\x63\x64\x65' + r'\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F\x70\x71\x72\x73\x74\x75' + r'\x76\x77\x78\x79\x7A\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x2B' + r'\x2F\x3D","","\x63\x68\x61\x72\x41\x74","\x69\x6E\x64\x65\x78' + r'\x4F\x66","\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65","' + r'\x6C\x65\x6E\x67\x74\x68"]') + +def detect(source): + """Detects MyObfuscate.com packer.""" + return SIGNATURE in source + +def unpack(source): + """Unpacks js code packed with MyObfuscate.com""" + if not detect(source): + return source + payload = unquote(_filter(source)) + match = re.search(r"^var _escape\=' - - - - - \ No newline at end of file diff --git a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.eot b/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.eot deleted file mode 100644 index 84677bc0..00000000 Binary files a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.eot and /dev/null differ diff --git a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.svg b/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.svg deleted file mode 100644 index d907b25a..00000000 --- a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.svg +++ /dev/null @@ -1,520 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.ttf b/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.ttf deleted file mode 100644 index 96a3639c..00000000 Binary files a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.woff b/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.woff deleted file mode 100644 index 628b6a52..00000000 Binary files a/mitmproxy/mitmproxy/webfonts/fontawesome-webfont.woff and /dev/null differ diff --git a/mitmproxy/models/__init__.py b/mitmproxy/models/__init__.py new file mode 100644 index 00000000..653b19fd --- /dev/null +++ b/mitmproxy/models/__init__.py @@ -0,0 +1,16 @@ +from __future__ import (absolute_import, print_function, division) + +from .http import ( + HTTPFlow, HTTPRequest, HTTPResponse, Headers, decoded, + make_error_response, make_connect_request, make_connect_response, expect_continue_response +) +from .connections import ClientConnection, ServerConnection +from .flow import Flow, Error + +__all__ = [ + "HTTPFlow", "HTTPRequest", "HTTPResponse", "Headers", "decoded", + "make_error_response", "make_connect_request", + "make_connect_response", "expect_continue_response", + "ClientConnection", "ServerConnection", + "Flow", "Error", +] diff --git a/mitmproxy/models/connections.py b/mitmproxy/models/connections.py new file mode 100644 index 00000000..d5920256 --- /dev/null +++ b/mitmproxy/models/connections.py @@ -0,0 +1,161 @@ +from __future__ import (absolute_import, print_function, division) + +import copy +import os + +from netlib import tcp, certutils +from .. import stateobject, utils + + +class ClientConnection(tcp.BaseHandler, stateobject.StateObject): + + def __init__(self, client_connection, address, server): + # Eventually, this object is restored from state. We don't have a + # connection then. + if client_connection: + super(ClientConnection, self).__init__(client_connection, address, server) + else: + self.connection = None + self.server = None + self.wfile = None + self.rfile = None + self.address = None + self.clientcert = None + self.ssl_established = None + + self.timestamp_start = utils.timestamp() + self.timestamp_end = None + self.timestamp_ssl_setup = None + self.protocol = None + + def __nonzero__(self): + return bool(self.connection) and not self.finished + + def __repr__(self): + return "".format( + ssl="[ssl] " if self.ssl_established else "", + address=repr(self.address) + ) + + @property + def tls_established(self): + return self.ssl_established + + _stateobject_attributes = dict( + address=tcp.Address, + clientcert=certutils.SSLCert, + ssl_established=bool, + timestamp_start=float, + timestamp_end=float, + timestamp_ssl_setup=float + ) + + def copy(self): + return copy.copy(self) + + def send(self, message): + if isinstance(message, list): + message = b''.join(message) + self.wfile.write(message) + self.wfile.flush() + + @classmethod + def from_state(cls, state): + f = cls(None, tuple(), None) + f.set_state(state) + return f + + def convert_to_ssl(self, *args, **kwargs): + super(ClientConnection, self).convert_to_ssl(*args, **kwargs) + self.timestamp_ssl_setup = utils.timestamp() + + def finish(self): + super(ClientConnection, self).finish() + self.timestamp_end = utils.timestamp() + + +class ServerConnection(tcp.TCPClient, stateobject.StateObject): + + def __init__(self, address, source_address=None): + tcp.TCPClient.__init__(self, address, source_address) + + self.via = None + self.timestamp_start = None + self.timestamp_end = None + self.timestamp_tcp_setup = None + self.timestamp_ssl_setup = None + self.protocol = None + + def __nonzero__(self): + return bool(self.connection) and not self.finished + + def __repr__(self): + if self.ssl_established and self.sni: + ssl = "[ssl: {0}] ".format(self.sni) + elif self.ssl_established: + ssl = "[ssl] " + else: + ssl = "" + return "".format( + ssl=ssl, + address=repr(self.address) + ) + + @property + def tls_established(self): + return self.ssl_established + + _stateobject_attributes = dict( + timestamp_start=float, + timestamp_end=float, + timestamp_tcp_setup=float, + timestamp_ssl_setup=float, + address=tcp.Address, + source_address=tcp.Address, + cert=certutils.SSLCert, + ssl_established=bool, + sni=str + ) + + @classmethod + def from_state(cls, state): + f = cls(tuple()) + f.set_state(state) + return f + + def copy(self): + return copy.copy(self) + + def connect(self): + self.timestamp_start = utils.timestamp() + tcp.TCPClient.connect(self) + self.timestamp_tcp_setup = utils.timestamp() + + def send(self, message): + if isinstance(message, list): + message = b''.join(message) + self.wfile.write(message) + self.wfile.flush() + + def establish_ssl(self, clientcerts, sni, **kwargs): + clientcert = None + if clientcerts: + if os.path.isfile(clientcerts): + clientcert = clientcerts + else: + path = os.path.join( + clientcerts, + self.address.host.encode("idna")) + ".pem" + if os.path.exists(path): + clientcert = path + + self.convert_to_ssl(cert=clientcert, sni=sni, **kwargs) + self.sni = sni + self.timestamp_ssl_setup = utils.timestamp() + + def finish(self): + tcp.TCPClient.finish(self) + self.timestamp_end = utils.timestamp() + + +ServerConnection._stateobject_attributes["via"] = ServerConnection diff --git a/mitmproxy/models/flow.py b/mitmproxy/models/flow.py new file mode 100644 index 00000000..10255dad --- /dev/null +++ b/mitmproxy/models/flow.py @@ -0,0 +1,171 @@ +from __future__ import (absolute_import, print_function, division) +import copy +import uuid + +from .. import stateobject, utils, version +from .connections import ClientConnection, ServerConnection + + +class Error(stateobject.StateObject): + + """ + An Error. + + This is distinct from an protocol error response (say, a HTTP code 500), + which is represented by a normal HTTPResponse object. This class is + responsible for indicating errors that fall outside of normal protocol + communications, like interrupted connections, timeouts, protocol errors. + + Exposes the following attributes: + + flow: Flow object + msg: Message describing the error + timestamp: Seconds since the epoch + """ + + def __init__(self, msg, timestamp=None): + """ + @type msg: str + @type timestamp: float + """ + self.flow = None # will usually be set by the flow backref mixin + self.msg = msg + self.timestamp = timestamp or utils.timestamp() + + _stateobject_attributes = dict( + msg=str, + timestamp=float + ) + + def __str__(self): + return self.msg + + @classmethod + def from_state(cls, state): + # the default implementation assumes an empty constructor. Override + # accordingly. + f = cls(None) + f.set_state(state) + return f + + def copy(self): + c = copy.copy(self) + return c + + +class Flow(stateobject.StateObject): + + """ + A Flow is a collection of objects representing a single transaction. + This class is usually subclassed for each protocol, e.g. HTTPFlow. + """ + + def __init__(self, type, client_conn, server_conn, live=None): + self.type = type + self.id = str(uuid.uuid4()) + self.client_conn = client_conn + """@type: ClientConnection""" + self.server_conn = server_conn + """@type: ServerConnection""" + self.live = live + """@type: LiveConnection""" + + self.error = None + """@type: Error""" + self.intercepted = False + """@type: bool""" + self._backup = None + self.reply = None + + _stateobject_attributes = dict( + id=str, + error=Error, + client_conn=ClientConnection, + server_conn=ServerConnection, + type=str, + intercepted=bool + ) + + def get_state(self): + d = super(Flow, self).get_state() + d.update(version=version.IVERSION) + if self._backup and self._backup != d: + d.update(backup=self._backup) + return d + + def set_state(self, state): + state.pop("version") + if "backup" in state: + self._backup = state.pop("backup") + super(Flow, self).set_state(state) + + def __eq__(self, other): + return self is other + + def copy(self): + f = copy.copy(self) + + f.id = str(uuid.uuid4()) + f.live = False + f.client_conn = self.client_conn.copy() + f.server_conn = self.server_conn.copy() + + if self.error: + f.error = self.error.copy() + return f + + def modified(self): + """ + Has this Flow been modified? + """ + if self._backup: + return self._backup != self.get_state() + else: + return False + + def backup(self, force=False): + """ + Save a backup of this Flow, which can be reverted to using a + call to .revert(). + """ + if not self._backup: + self._backup = self.get_state() + + def revert(self): + """ + Revert to the last backed up state. + """ + if self._backup: + self.set_state(self._backup) + self._backup = None + + def kill(self, master): + """ + Kill this request. + """ + from ..protocol import Kill + + self.error = Error("Connection killed") + self.intercepted = False + self.reply(Kill) + master.handle_error(self) + + def intercept(self, master): + """ + Intercept this Flow. Processing will stop until accept_intercept is + called. + """ + if self.intercepted: + return + self.intercepted = True + master.handle_intercept(self) + + def accept_intercept(self, master): + """ + Continue with the flow - called after an intercept(). + """ + if not self.intercepted: + return + self.intercepted = False + self.reply() + master.handle_accept_intercept(self) diff --git a/mitmproxy/models/http.py b/mitmproxy/models/http.py new file mode 100644 index 00000000..394fe51a --- /dev/null +++ b/mitmproxy/models/http.py @@ -0,0 +1,469 @@ +from __future__ import (absolute_import, print_function, division) +import Cookie +import copy +import warnings +from email.utils import parsedate_tz, formatdate, mktime_tz +import time + +from netlib import encoding +from netlib.http import status_codes, Headers, Request, Response, decoded +from netlib.tcp import Address +from .. import utils +from .. import version +from .flow import Flow + + +class MessageMixin(object): + + def get_decoded_content(self): + """ + Returns the decoded content based on the current Content-Encoding + header. + Doesn't change the message iteself or its headers. + """ + ce = self.headers.get("content-encoding") + if not self.content or ce not in encoding.ENCODINGS: + return self.content + return encoding.decode(ce, self.content) + + def copy(self): + c = copy.copy(self) + if hasattr(self, "data"): # FIXME remove condition + c.data = copy.copy(self.data) + + c.headers = self.headers.copy() + return c + + def replace(self, pattern, repl, *args, **kwargs): + """ + Replaces a regular expression pattern with repl in both the headers + and the body of the message. Encoded body will be decoded + before replacement, and re-encoded afterwards. + + Returns the number of replacements made. + """ + count = 0 + if self.content: + with decoded(self): + self.content, count = utils.safe_subn( + pattern, repl, self.content, *args, **kwargs + ) + fields = [] + for name, value in self.headers.fields: + name, c = utils.safe_subn(pattern, repl, name, *args, **kwargs) + count += c + value, c = utils.safe_subn(pattern, repl, value, *args, **kwargs) + count += c + fields.append([name, value]) + self.headers.fields = fields + return count + + +class HTTPRequest(MessageMixin, Request): + + """ + An HTTP request. + + Exposes the following attributes: + + method: HTTP method + + scheme: URL scheme (http/https) + + host: Target hostname of the request. This is not neccessarily the + directy upstream server (which could be another proxy), but it's always + the target server we want to reach at the end. This attribute is either + inferred from the request itself (absolute-form, authority-form) or from + the connection metadata (e.g. the host in reverse proxy mode). + + port: Destination port + + path: Path portion of the URL (not present in authority-form) + + http_version: HTTP version, e.g. "HTTP/1.1" + + headers: Headers object + + content: Content of the request, None, or CONTENT_MISSING if there + is content associated, but not present. CONTENT_MISSING evaluates + to False to make checking for the presence of content natural. + + form_in: The request form which mitmproxy has received. The following + values are possible: + + - relative (GET /index.html, OPTIONS *) (covers origin form and + asterisk form) + - absolute (GET http://example.com:80/index.html) + - authority-form (CONNECT example.com:443) + Details: http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-25#section-5.3 + + form_out: The request form which mitmproxy will send out to the + destination + + timestamp_start: Timestamp indicating when request transmission started + + timestamp_end: Timestamp indicating when request transmission ended + """ + + def __init__( + self, + first_line_format, + method, + scheme, + host, + port, + path, + http_version, + headers, + content, + timestamp_start=None, + timestamp_end=None, + form_out=None, + is_replay=False, + stickycookie=False, + stickyauth=False, + ): + Request.__init__( + self, + first_line_format, + method, + scheme, + host, + port, + path, + http_version, + headers, + content, + timestamp_start, + timestamp_end, + ) + self.form_out = form_out or first_line_format # FIXME remove + + # Have this request's cookies been modified by sticky cookies or auth? + self.stickycookie = stickycookie + self.stickyauth = stickyauth + + # Is this request replayed? + self.is_replay = is_replay + + def get_state(self): + state = super(HTTPRequest, self).get_state() + state.update( + stickycookie = self.stickycookie, + stickyauth = self.stickyauth, + is_replay = self.is_replay, + ) + return state + + def set_state(self, state): + self.stickycookie = state.pop("stickycookie") + self.stickyauth = state.pop("stickyauth") + self.is_replay = state.pop("is_replay") + super(HTTPRequest, self).set_state(state) + + @classmethod + def wrap(self, request): + req = HTTPRequest( + first_line_format=request.form_in, + method=request.method, + scheme=request.scheme, + host=request.host, + port=request.port, + path=request.path, + http_version=request.http_version, + headers=request.headers, + content=request.content, + timestamp_start=request.timestamp_start, + timestamp_end=request.timestamp_end, + form_out=(request.form_out if hasattr(request, 'form_out') else None), + ) + return req + + @property + def form_out(self): + warnings.warn(".form_out is deprecated, use .first_line_format instead.", DeprecationWarning) + return self.first_line_format + + @form_out.setter + def form_out(self, value): + warnings.warn(".form_out is deprecated, use .first_line_format instead.", DeprecationWarning) + self.first_line_format = value + + def __hash__(self): + return id(self) + + def replace(self, pattern, repl, *args, **kwargs): + """ + Replaces a regular expression pattern with repl in the headers, the + request path and the body of the request. Encoded content will be + decoded before replacement, and re-encoded afterwards. + + Returns the number of replacements made. + """ + c = MessageMixin.replace(self, pattern, repl, *args, **kwargs) + self.path, pc = utils.safe_subn( + pattern, repl, self.path, *args, **kwargs + ) + c += pc + return c + + +class HTTPResponse(MessageMixin, Response): + + """ + An HTTP response. + + Exposes the following attributes: + + http_version: HTTP version, e.g. "HTTP/1.1" + + status_code: HTTP response status code + + msg: HTTP response message + + headers: Headers object + + content: Content of the request, None, or CONTENT_MISSING if there + is content associated, but not present. CONTENT_MISSING evaluates + to False to make checking for the presence of content natural. + + timestamp_start: Timestamp indicating when request transmission started + + timestamp_end: Timestamp indicating when request transmission ended + """ + + def __init__( + self, + http_version, + status_code, + reason, + headers, + content, + timestamp_start=None, + timestamp_end=None, + is_replay = False + ): + Response.__init__( + self, + http_version, + status_code, + reason, + headers, + content, + timestamp_start=timestamp_start, + timestamp_end=timestamp_end, + ) + + # Is this request replayed? + self.is_replay = is_replay + self.stream = False + + @classmethod + def wrap(self, response): + resp = HTTPResponse( + http_version=response.http_version, + status_code=response.status_code, + reason=response.reason, + headers=response.headers, + content=response.content, + timestamp_start=response.timestamp_start, + timestamp_end=response.timestamp_end, + ) + return resp + + def _refresh_cookie(self, c, delta): + """ + Takes a cookie string c and a time delta in seconds, and returns + a refreshed cookie string. + """ + try: + c = Cookie.SimpleCookie(str(c)) + except Cookie.CookieError: + raise ValueError("Invalid Cookie") + for i in c.values(): + if "expires" in i: + d = parsedate_tz(i["expires"]) + if d: + d = mktime_tz(d) + delta + i["expires"] = formatdate(d) + else: + # This can happen when the expires tag is invalid. + # reddit.com sends a an expires tag like this: "Thu, 31 Dec + # 2037 23:59:59 GMT", which is valid RFC 1123, but not + # strictly correct according to the cookie spec. Browsers + # appear to parse this tolerantly - maybe we should too. + # For now, we just ignore this. + del i["expires"] + ret = c.output(header="").strip() + if not ret: + raise ValueError("Invalid Cookie") + return ret + + def refresh(self, now=None): + """ + This fairly complex and heuristic function refreshes a server + response for replay. + + - It adjusts date, expires and last-modified headers. + - It adjusts cookie expiration. + """ + if not now: + now = time.time() + delta = now - self.timestamp_start + refresh_headers = [ + "date", + "expires", + "last-modified", + ] + for i in refresh_headers: + if i in self.headers: + d = parsedate_tz(self.headers[i]) + if d: + new = mktime_tz(d) + delta + self.headers[i] = formatdate(new) + c = [] + for set_cookie_header in self.headers.get_all("set-cookie"): + try: + refreshed = self._refresh_cookie(set_cookie_header, delta) + except ValueError: + refreshed = set_cookie_header + c.append(refreshed) + if c: + self.headers.set_all("set-cookie", c) + + +class HTTPFlow(Flow): + + """ + A HTTPFlow is a collection of objects representing a single HTTP + transaction. + + Attributes: + request: HTTPRequest object + response: HTTPResponse object + error: Error object + server_conn: ServerConnection object + client_conn: ClientConnection object + intercepted: Is this flow currently being intercepted? + live: Does this flow have a live client connection? + + Note that it's possible for a Flow to have both a response and an error + object. This might happen, for instance, when a response was received + from the server, but there was an error sending it back to the client. + """ + + def __init__(self, client_conn, server_conn, live=None): + super(HTTPFlow, self).__init__("http", client_conn, server_conn, live) + self.request = None + """@type: HTTPRequest""" + self.response = None + """@type: HTTPResponse""" + + _stateobject_attributes = Flow._stateobject_attributes.copy() + _stateobject_attributes.update( + request=HTTPRequest, + response=HTTPResponse + ) + + @classmethod + def from_state(cls, state): + f = cls(None, None) + f.set_state(state) + return f + + def __repr__(self): + s = " + + %d %s + + %s + + """.strip() % (status_code, response, message) + + if not headers: + headers = Headers( + Server=version.NAMEVERSION, + Connection="close", + Content_Length=str(len(body)), + Content_Type="text/html" + ) + + return HTTPResponse( + b"HTTP/1.1", + status_code, + response, + headers, + body, + ) + + +def make_connect_request(address): + address = Address.wrap(address) + return HTTPRequest( + "authority", "CONNECT", None, address.host, address.port, None, b"HTTP/1.1", + Headers(), "" + ) + + +def make_connect_response(http_version): + # Do not send any response headers as it breaks proxying non-80 ports on + # Android emulators using the -http-proxy option. + return HTTPResponse( + http_version, + 200, + "Connection established", + Headers(), + "", + ) + +expect_continue_response = HTTPResponse(b"HTTP/1.1", 100, "Continue", Headers(), b"") diff --git a/mitmproxy/onboarding/__init__.py b/mitmproxy/onboarding/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mitmproxy/onboarding/app.py b/mitmproxy/onboarding/app.py new file mode 100644 index 00000000..ff5ed63c --- /dev/null +++ b/mitmproxy/onboarding/app.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import +import os +import tornado.web +import tornado.wsgi +import tornado.template + +from .. import utils +from ..proxy import config + + +loader = tornado.template.Loader(utils.pkg_data.path("onboarding/templates")) + + +class Adapter(tornado.wsgi.WSGIAdapter): + # Tornado doesn't make the WSGI environment available to pages, so this + # hideous monkey patch is the easiest way to get to the mitmproxy.master + # variable. + + def __init__(self, application): + self._application = application + + def application(self, request): + request.master = self.environ["mitmproxy.master"] + return self._application(request) + + def __call__(self, environ, start_response): + self.environ = environ + return tornado.wsgi.WSGIAdapter.__call__( + self, + environ, + start_response + ) + + +class Index(tornado.web.RequestHandler): + + def get(self): + t = loader.load("index.html") + self.write(t.generate()) + + +class PEM(tornado.web.RequestHandler): + + @property + def filename(self): + return config.CONF_BASENAME + "-ca-cert.pem" + + def get(self): + p = os.path.join(self.request.master.server.config.cadir, self.filename) + self.set_header("Content-Type", "application/x-x509-ca-cert") + self.set_header( + "Content-Disposition", + "inline; filename={}".format( + self.filename)) + + with open(p, "rb") as f: + self.write(f.read()) + + +class P12(tornado.web.RequestHandler): + + @property + def filename(self): + return config.CONF_BASENAME + "-ca-cert.p12" + + def get(self): + p = os.path.join(self.request.master.server.config.cadir, self.filename) + self.set_header("Content-Type", "application/x-pkcs12") + self.set_header( + "Content-Disposition", + "inline; filename={}".format( + self.filename)) + + with open(p, "rb") as f: + self.write(f.read()) + + +application = tornado.web.Application( + [ + (r"/", Index), + (r"/cert/pem", PEM), + (r"/cert/p12", P12), + ( + r"/static/(.*)", + tornado.web.StaticFileHandler, + { + "path": utils.pkg_data.path("onboarding/static") + } + ), + ], + # debug=True +) +mapp = Adapter(application) diff --git a/mitmproxy/onboarding/static/bootstrap.min.css b/mitmproxy/onboarding/static/bootstrap.min.css new file mode 100644 index 00000000..f31489f9 --- /dev/null +++ b/mitmproxy/onboarding/static/bootstrap.min.css @@ -0,0 +1 @@ +/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Open Sans",Calibri,Candara,Arial,sans-serif;font-size:15px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#007fff;text-decoration:none}a:hover,a:focus{color:#0059b3;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:0}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border:0;border-top:1px solid #e6e6e6}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Open Sans",Calibri,Candara,Arial,sans-serif;font-weight:300;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:21px;margin-bottom:10.5px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10.5px;margin-bottom:10.5px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:39px}h2,.h2{font-size:32px}h3,.h3{font-size:26px}h4,.h4{font-size:19px}h5,.h5{font-size:15px}h6,.h6{font-size:13px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:17px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:22.5px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#007fff}.text-primary:hover{color:#06c}.text-warning{color:#fff}.text-warning:hover{color:#e6e6e6}.text-danger{color:#fff}.text-danger:hover{color:#e6e6e6}.text-success{color:#fff}.text-success:hover{color:#e6e6e6}.text-info{color:#fff}.text-info:hover{color:#e6e6e6}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid #e6e6e6}ul,ol{margin-top:0;margin-bottom:10.5px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:21px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10.5px 21px;margin:0 0 21px;border-left:5px solid #e6e6e6}blockquote p{font-size:18.75px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #e6e6e6;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:21px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:0}pre{display:block;padding:10px;margin:0 0 10.5px;font-size:14px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:21px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#3fb618}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#379f15}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#ff0039}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#e60033}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#ff7518}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#fe6600}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15.75px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:21px;font-size:22.5px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:11px;font-size:15px;line-height:1.428571429;color:#333;vertical-align:middle}.form-control{display:block;width:100%;height:43px;padding:10px 18px;font-size:15px;line-height:1.428571429;color:#333;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#e6e6e6}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:21px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:31px;padding:5px 10px;font-size:13px;line-height:1.5;border-radius:0}select.input-sm{height:31px;line-height:31px}textarea.input-sm{height:auto}.input-lg{height:64px;padding:18px 30px;font-size:19px;line-height:1.33;border-radius:0}select.input-lg{height:64px;line-height:64px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#fff}.has-warning .form-control{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#fff;background-color:#ff7518;border-color:#fff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#fff}.has-error .form-control{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-error .input-group-addon{color:#fff;background-color:#ff0039;border-color:#fff}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#fff}.has-success .form-control{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-success .input-group-addon{color:#fff;background-color:#3fb618;border-color:#fff}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:11px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:32px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:11px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:10px 18px;margin-bottom:0;font-size:15px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#fff;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#fff;background-color:#222;border-color:#222}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#fff;background-color:#0e0e0e;border-color:#040404}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#222;border-color:#222}.btn-default .badge{color:#222;background-color:#fff}.btn-primary{color:#fff;background-color:#007fff;border-color:#007fff}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#006bd6;border-color:#0061c2}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#007fff;border-color:#007fff}.btn-primary .badge{color:#007fff;background-color:#fff}.btn-warning{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ee6000;border-color:#da5800}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#ff7518;border-color:#ff7518}.btn-warning .badge{color:#ff7518;background-color:#fff}.btn-danger{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d60030;border-color:#c2002b}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#ff0039;border-color:#ff0039}.btn-danger .badge{color:#ff0039;background-color:#fff}.btn-success{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#339213;border-color:#2c8011}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#3fb618;border-color:#3fb618}.btn-success .badge{color:#3fb618;background-color:#fff}.btn-info{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#8441a5;border-color:#783c96}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#9954bb;border-color:#9954bb}.btn-info .badge{color:#9954bb;background-color:#fff}.btn-link{font-weight:normal;color:#007fff;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#0059b3;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:18px 30px;font-size:19px;line-height:1.33;border-radius:0}.btn-sm{padding:5px 10px;font-size:13px;line-height:1.5;border-radius:0}.btn-xs{padding:1px 5px;font-size:13px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:15px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#fff;text-decoration:none;background-color:#007fff}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#007fff;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:13px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:13px;line-height:1.5;border-radius:0}.btn-group-sm>.btn{padding:5px 10px;font-size:13px;line-height:1.5;border-radius:0}.btn-group-lg>.btn{padding:18px 30px;font-size:19px;line-height:1.33;border-radius:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:64px;padding:18px 30px;font-size:19px;line-height:1.33;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:64px;line-height:64px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:31px;padding:5px 10px;font-size:13px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:31px;line-height:31px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:10px 18px;font-size:15px;font-weight:normal;line-height:1;color:#333;text-align:center;background-color:#e6e6e6;border:1px solid #ccc;border-radius:0}.input-group-addon.input-sm{padding:5px 10px;font-size:13px;border-radius:0}.input-group-addon.input-lg{padding:18px 30px;font-size:19px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#e6e6e6}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#e6e6e6;border-color:#007fff}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:0}.nav-tabs>li>a:hover{border-color:#e6e6e6 #e6e6e6 #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#007fff}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:21px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:0}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:14.5px 15px;font-size:19px;line-height:21px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.25px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:14.5px;padding-bottom:14.5px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:3.5px;margin-right:-15px;margin-bottom:3.5px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:3.5px;margin-bottom:3.5px}.navbar-btn.btn-sm{margin-top:9.5px;margin-bottom:9.5px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:14.5px;margin-bottom:14.5px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#222;border-color:#121212}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#fff;background-color:none}.navbar-default .navbar-text{color:#fff}.navbar-default .navbar-nav>li>a{color:#fff}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#fff;background-color:#090909}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;background-color:#090909}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:transparent}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#090909}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#121212}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#fff;background-color:#090909}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:#090909}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#090909}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#fff}.navbar-default .navbar-link:hover{color:#fff}.navbar-inverse{background-color:#007fff;border-color:#06c}.navbar-inverse .navbar-brand{color:#fff}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:none}.navbar-inverse .navbar-text{color:#fff}.navbar-inverse .navbar-nav>li>a{color:#fff}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:#06c}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#06c}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:transparent}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#06c}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#006ddb}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#06c}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#06c}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#06c}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:#06c}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#06c}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#fff;background-color:transparent}}.navbar-inverse .navbar-link{color:#fff}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:21px;list-style:none;background-color:#f5f5f5;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:21px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:10px 18px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#e6e6e6}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#999;cursor:default;background-color:#f5f5f5;border-color:#f5f5f5}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:18px 30px;font-size:19px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:13px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pager{padding-left:0;margin:21px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:0}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#e6e6e6}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#222}.label-default[href]:hover,.label-default[href]:focus{background-color:#090909}.label-primary{background-color:#007fff}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#06c}.label-success{background-color:#3fb618}.label-success[href]:hover,.label-success[href]:focus{background-color:#2f8912}.label-info{background-color:#9954bb}.label-info[href]:hover,.label-info[href]:focus{background-color:#7e3f9d}.label-warning{background-color:#ff7518}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#e45c00}.label-danger{background-color:#ff0039}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#cc002e}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:13px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#007fff;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:23px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#e6e6e6}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:0}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:67.5px}}.thumbnail{display:block;padding:4px;margin-bottom:21px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#007fff}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:21px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#fff;background-color:#3fb618;border-color:#4e9f15}.alert-success hr{border-top-color:#438912}.alert-success .alert-link{color:#e6e6e6}.alert-info{color:#fff;background-color:#9954bb;border-color:#7643a8}.alert-info hr{border-top-color:#693c96}.alert-info .alert-link{color:#e6e6e6}.alert-warning{color:#fff;background-color:#ff7518;border-color:#ff4309}.alert-warning hr{border-top-color:#ee3800}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#fff;background-color:#ff0039;border-color:#f0005e}.alert-danger hr{border-top-color:#d60054}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:21px;margin-bottom:21px;overflow:hidden;background-color:#ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:13px;line-height:21px;color:#fff;text-align:center;background-color:#007fff;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#3fb618}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#9954bb}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#ff7518}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#ff0039}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#007fff;border-color:#007fff}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#cce5ff}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:21px;background-color:#fff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:-1px;border-top-left-radius:-1px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:17px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:0}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#007fff}.panel-primary>.panel-heading{color:#fff;background-color:#007fff;border-color:#007fff}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#007fff}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#007fff}.panel-success{border-color:#4e9f15}.panel-success>.panel-heading{color:#fff;background-color:#3fb618;border-color:#4e9f15}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#4e9f15}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#4e9f15}.panel-warning{border-color:#ff4309}.panel-warning>.panel-heading{color:#fff;background-color:#ff7518;border-color:#ff4309}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#ff4309}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ff4309}.panel-danger{border-color:#f0005e}.panel-danger>.panel-heading{color:#fff;background-color:#ff0039;border-color:#f0005e}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#f0005e}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#f0005e}.panel-info{border-color:#7643a8}.panel-info>.panel-heading{color:#fff;background-color:#9954bb;border-color:#7643a8}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#7643a8}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#7643a8}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:22.5px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:0;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:13px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:rgba(0,0,0,0.9);border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:rgba(0,0,0,0.9);border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:rgba(0,0,0,0.9);border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:rgba(0,0,0,0.9);border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:rgba(0,0,0,0.9);border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:rgba(0,0,0,0.9);border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:rgba(0,0,0,0.9);border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:rgba(0,0,0,0.9);border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:rgba(0,0,0,0.9);border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:0;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:15px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}}.btn{border:0}.text-primary,.text-primary:hover{color:#007fff}.text-success,.text-success:hover{color:#3fb618}.text-danger,.text-danger:hover{color:#ff0039}.text-warning,.text-warning:hover{color:#ff7518}.text-info,.text-info:hover{color:#9954bb}.table tr.success,.table tr.warning,.table tr.danger{color:#fff}.has-warning .help-block,.has-warning .control-label{color:#ff7518}.has-warning .form-control,.has-warning .form-control:focus{border:1px solid #ff7518}.has-error .help-block,.has-error .control-label{color:#ff0039}.has-error .form-control,.has-error .form-control:focus{border:1px solid #ff0039}.has-success .help-block,.has-success .control-label{color:#3fb618}.has-success .form-control,.has-success .form-control:focus{border:1px solid #3fb618}.nav-pills>li>a{border-radius:0}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:none}.pagination .active>a,.pagination .active>a:hover{border-color:#ddd}.alert{border:0}.alert .alert-link{color:#fff;text-decoration:underline}.label{border-radius:0}.close{opacity:1}.progress{height:8px;-webkit-box-shadow:none;box-shadow:none}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed} diff --git a/mitmproxy/onboarding/static/fontawesome/css/font-awesome.css b/mitmproxy/onboarding/static/fontawesome/css/font-awesome.css new file mode 100644 index 00000000..048cff97 --- /dev/null +++ b/mitmproxy/onboarding/static/fontawesome/css/font-awesome.css @@ -0,0 +1,1338 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.0.3'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.3333333333333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.2857142857142858em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.142857142857143em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.142857142857143em; + width: 2.142857142857143em; + top: 0.14285714285714285em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.8571428571428572em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(359deg); + } +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + } +} +@-o-keyframes spin { + 0% { + -o-transform: rotate(0deg); + } + 100% { + -o-transform: rotate(359deg); + } +} +@-ms-keyframes spin { + 0% { + -ms-transform: rotate(0deg); + } + 100% { + -ms-transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -moz-transform: scale(1, -1); + -ms-transform: scale(1, -1); + -o-transform: scale(1, -1); + transform: scale(1, -1); +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-asc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-desc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-reply-all:before { + content: "\f122"; +} +.fa-mail-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} diff --git a/mitmproxy/onboarding/static/fontawesome/css/font-awesome.min.css b/mitmproxy/onboarding/static/fontawesome/css/font-awesome.min.css new file mode 100644 index 00000000..449d6ac5 --- /dev/null +++ b/mitmproxy/onboarding/static/fontawesome/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} \ No newline at end of file diff --git a/mitmproxy/onboarding/static/fontawesome/fonts/FontAwesome.otf b/mitmproxy/onboarding/static/fontawesome/fonts/FontAwesome.otf new file mode 100644 index 00000000..8b0f54e4 Binary files /dev/null and b/mitmproxy/onboarding/static/fontawesome/fonts/FontAwesome.otf differ diff --git a/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.eot b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.eot new file mode 100644 index 00000000..7c79c6a6 Binary files /dev/null and b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.eot differ diff --git a/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.svg b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.svg new file mode 100644 index 00000000..45fdf338 --- /dev/null +++ b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.svg @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.ttf b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..e89738de Binary files /dev/null and b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.ttf differ diff --git a/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.woff b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.woff new file mode 100644 index 00000000..8c1748aa Binary files /dev/null and b/mitmproxy/onboarding/static/fontawesome/fonts/fontawesome-webfont.woff differ diff --git a/mitmproxy/onboarding/static/mitmproxy.css b/mitmproxy/onboarding/static/mitmproxy.css new file mode 100644 index 00000000..b390976a --- /dev/null +++ b/mitmproxy/onboarding/static/mitmproxy.css @@ -0,0 +1,48 @@ + +#certbank div { + text-align: center; + + +} + +.fronttable { +} + +.bigtitle { + font-weight: bold; + font-size: 50px; + line-height: 55px; + text-align: center; + display: table; + height: 300px; +} + +.bigtitle>div { + display: table-cell; + vertical-align: middle; +} + +section { + margin-top: 50px; +} + +.example { + margin-top: 10px; + margin-bottom: 10px; +} + +.innerlink { + text-decoration: none; + border-bottom:1px dotted; + margin-bottom: 15px; +} + +.masthead { + padding: 50px 0 60px; + text-align: center; + +} + +.header { + font-size: 1.5em; +} diff --git a/mitmproxy/onboarding/templates/frame.html b/mitmproxy/onboarding/templates/frame.html new file mode 100644 index 00000000..f00e1a66 --- /dev/null +++ b/mitmproxy/onboarding/templates/frame.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% block content %} +
+
+ {% block body %} + {% end %} +
+
+{% end %} diff --git a/mitmproxy/onboarding/templates/index.html b/mitmproxy/onboarding/templates/index.html new file mode 100644 index 00000000..1bcff1b8 --- /dev/null +++ b/mitmproxy/onboarding/templates/index.html @@ -0,0 +1,35 @@ +{% extends "frame.html" %} +{% block body %} + +
+

Click to install the mitmproxy certificate:

+
+
+
+ +

Apple

+
+
+ +

Windows

+
+
+ +

Android

+
+
+ +

Other

+
+
+ +
+
+ Other mitmproxy users cannot intercept your connection. +
+
+ This page is served by your local mitmproxy instance. The certificate you are about to install has been uniquely generated on mitmproxy's first run and is not shared + between mitmproxy installations. +
+ +{% end %} diff --git a/mitmproxy/onboarding/templates/layout.html b/mitmproxy/onboarding/templates/layout.html new file mode 100644 index 00000000..8726a788 --- /dev/null +++ b/mitmproxy/onboarding/templates/layout.html @@ -0,0 +1,32 @@ + + + + + + + + + + + mitmproxy + + + + + + + + +
+ {% block content %} + {% end %} +
+ + + diff --git a/mitmproxy/platform/__init__.py b/mitmproxy/platform/__init__.py new file mode 100644 index 00000000..e1ff7c47 --- /dev/null +++ b/mitmproxy/platform/__init__.py @@ -0,0 +1,16 @@ +import sys + +resolver = None + +if sys.platform == "linux2": + from . import linux + resolver = linux.Resolver +elif sys.platform == "darwin": + from . import osx + resolver = osx.Resolver +elif sys.platform.startswith("freebsd"): + from . import osx + resolver = osx.Resolver +elif sys.platform == "win32": + from . import windows + resolver = windows.Resolver diff --git a/mitmproxy/platform/linux.py b/mitmproxy/platform/linux.py new file mode 100644 index 00000000..38bfbe42 --- /dev/null +++ b/mitmproxy/platform/linux.py @@ -0,0 +1,14 @@ +import socket +import struct + +# Python socket module does not have this constant +SO_ORIGINAL_DST = 80 + + +class Resolver(object): + + def original_addr(self, csock): + odestdata = csock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) + _, port, a1, a2, a3, a4 = struct.unpack("!HHBBBBxxxxxxxx", odestdata) + address = "%d.%d.%d.%d" % (a1, a2, a3, a4) + return address, port diff --git a/mitmproxy/platform/osx.py b/mitmproxy/platform/osx.py new file mode 100644 index 00000000..afbc919b --- /dev/null +++ b/mitmproxy/platform/osx.py @@ -0,0 +1,36 @@ +import subprocess +import pf + +""" + Doing this the "right" way by using DIOCNATLOOK on the pf device turns out + to be a pain. Apple has made a number of modifications to the data + structures returned, and compiling userspace tools to test and work with + this turns out to be a pain in the ass. Parsing pfctl output is short, + simple, and works. + + Note: Also Tested with FreeBSD 10 pkgng Python 2.7.x. + Should work almost exactly as on Mac OS X and except with some changes to + the output processing of pfctl (see pf.py). +""" + + +class Resolver(object): + STATECMD = ("sudo", "-n", "/sbin/pfctl", "-s", "state") + + def original_addr(self, csock): + peer = csock.getpeername() + try: + stxt = subprocess.check_output(self.STATECMD, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + if "sudo: a password is required" in e.output: + insufficient_priv = True + else: + raise RuntimeError("Error getting pfctl state: " + repr(e)) + else: + insufficient_priv = "sudo: a password is required" in stxt + + if insufficient_priv: + raise RuntimeError( + "Insufficient privileges to access pfctl. " + "See http://mitmproxy.org/doc/transparent/osx.html for details.") + return pf.lookup(peer[0], peer[1], stxt) diff --git a/mitmproxy/platform/pf.py b/mitmproxy/platform/pf.py new file mode 100644 index 00000000..97a4c192 --- /dev/null +++ b/mitmproxy/platform/pf.py @@ -0,0 +1,24 @@ +import sys + + +def lookup(address, port, s): + """ + Parse the pfctl state output s, to look up the destination host + matching the client (address, port). + + Returns an (address, port) tuple, or None. + """ + spec = "%s:%s" % (address, port) + for i in s.split("\n"): + if "ESTABLISHED:ESTABLISHED" in i and spec in i: + s = i.split() + if len(s) > 4: + if sys.platform.startswith("freebsd"): + # strip parentheses for FreeBSD pfctl + s = s[3][1:-1].split(":") + else: + s = s[4].split(":") + + if len(s) == 2: + return s[0], int(s[1]) + raise RuntimeError("Could not resolve original destination.") diff --git a/mitmproxy/platform/windows.py b/mitmproxy/platform/windows.py new file mode 100644 index 00000000..9fe04cfa --- /dev/null +++ b/mitmproxy/platform/windows.py @@ -0,0 +1,432 @@ +import configargparse +import cPickle as pickle +from ctypes import byref, windll, Structure +from ctypes.wintypes import DWORD +import os +import socket +import SocketServer +import struct +import threading +import time +from collections import OrderedDict + +from pydivert.windivert import WinDivert +from pydivert.enum import Direction, Layer, Flag + + +PROXY_API_PORT = 8085 + + +class Resolver(object): + + def __init__(self): + TransparentProxy.setup() + self.socket = None + self.lock = threading.RLock() + self._connect() + + def _connect(self): + if self.socket: + self.socket.close() + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect(("127.0.0.1", PROXY_API_PORT)) + + self.wfile = self.socket.makefile('wb') + self.rfile = self.socket.makefile('rb') + pickle.dump(os.getpid(), self.wfile) + + def original_addr(self, csock): + client = csock.getpeername()[:2] + with self.lock: + try: + pickle.dump(client, self.wfile) + self.wfile.flush() + addr = pickle.load(self.rfile) + if addr is None: + raise RuntimeError("Cannot resolve original destination.") + addr = list(addr) + addr[0] = str(addr[0]) + addr = tuple(addr) + return addr + except (EOFError, socket.error): + self._connect() + return self.original_addr(csock) + + +class APIRequestHandler(SocketServer.StreamRequestHandler): + + """ + TransparentProxy API: Returns the pickled server address, port tuple + for each received pickled client address, port tuple. + """ + + def handle(self): + proxifier = self.server.proxifier + pid = None + try: + pid = pickle.load(self.rfile) + if pid is not None: + proxifier.trusted_pids.add(pid) + + while True: + client = pickle.load(self.rfile) + server = proxifier.client_server_map.get(client, None) + pickle.dump(server, self.wfile) + self.wfile.flush() + + except (EOFError, socket.error): + proxifier.trusted_pids.discard(pid) + + +class APIServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + + def __init__(self, proxifier, *args, **kwargs): + SocketServer.TCPServer.__init__(self, *args, **kwargs) + self.proxifier = proxifier + self.daemon_threads = True + + +# Windows error.h +ERROR_INSUFFICIENT_BUFFER = 0x7A + + +# http://msdn.microsoft.com/en-us/library/windows/desktop/bb485761(v=vs.85).aspx +class MIB_TCPROW2(Structure): + _fields_ = [ + ('dwState', DWORD), + ('dwLocalAddr', DWORD), + ('dwLocalPort', DWORD), + ('dwRemoteAddr', DWORD), + ('dwRemotePort', DWORD), + ('dwOwningPid', DWORD), + ('dwOffloadState', DWORD) + ] + + +# http://msdn.microsoft.com/en-us/library/windows/desktop/bb485772(v=vs.85).aspx +def MIB_TCPTABLE2(size): + class _MIB_TCPTABLE2(Structure): + _fields_ = [('dwNumEntries', DWORD), + ('table', MIB_TCPROW2 * size)] + + return _MIB_TCPTABLE2() + + +class TransparentProxy(object): + + """ + Transparent Windows Proxy for mitmproxy based on WinDivert/PyDivert. + + Requires elevated (admin) privileges. Can be started separately by manually running the file. + + This module can be used to intercept and redirect all traffic that is forwarded by the user's machine and + traffic sent from the machine itself. + + How it works: + + (1) First, we intercept all packages that match our filter (destination port 80 and 443 by default). + We both consider traffic that is forwarded by the OS (WinDivert's NETWORK_FORWARD layer) as well as traffic + sent from the local machine (WinDivert's NETWORK layer). In the case of traffic from the local machine, we need to + distinguish between traffc sent from applications and traffic sent from the proxy. To accomplish this, we use + Windows' GetTcpTable2 syscall to determine the source application's PID. + + For each intercepted package, we + 1. Store the source -> destination mapping (address and port) + 2. Remove the package from the network (by not reinjecting it). + 3. Re-inject the package into the local network stack, but with the destination address changed to the proxy. + + (2) Next, the proxy receives the forwarded packet, but does not know the real destination yet (which we overwrote + with the proxy's address). On Linux, we would now call getsockopt(SO_ORIGINAL_DST), but that unfortunately doesn't + work on Windows. However, we still have the correct source information. As a workaround, we now access the forward + module's API (see APIRequestHandler), submit the source information and get the actual destination back (which the + forward module stored in (1.3)). + + (3) The proxy now establish the upstream connection as usual. + + (4) Finally, the proxy sends the response back to the client. To make it work, we need to change the packet's source + address back to the original destination (using the mapping from (1.3)), to which the client believes he is talking + to. + + Limitations: + + - No IPv6 support. (Pull Requests welcome) + - TCP ports do not get re-used simulateously on the client, i.e. the proxy will fail if application X + connects to example.com and example.org from 192.168.0.42:4242 simultaneously. This could be mitigated by + introducing unique "meta-addresses" which mitmproxy sees, but this would remove the correct client info from + mitmproxy. + + """ + + def __init__(self, + mode="both", + redirect_ports=(80, 443), custom_filter=None, + proxy_addr=False, proxy_port=8080, + api_host="localhost", api_port=PROXY_API_PORT, + cache_size=65536): + """ + :param mode: Redirection operation mode: "forward" to only redirect forwarded packets, "local" to only redirect + packets originating from the local machine, "both" to redirect both. + :param redirect_ports: if the destination port is in this tuple, the requests are redirected to the proxy. + :param custom_filter: specify a custom WinDivert filter to select packets that should be intercepted. Overrides + redirect_ports setting. + :param proxy_addr: IP address of the proxy (IP within a network, 127.0.0.1 does not work). By default, + this is detected automatically. + :param proxy_port: Port the proxy is listenting on. + :param api_host: Host the forward module API is listening on. + :param api_port: Port the forward module API is listening on. + :param cache_size: Maximum number of connection tuples that are stored. Only relevant in very high + load scenarios. + """ + if proxy_port in redirect_ports: + raise ValueError("The proxy port must not be a redirect port.") + + if not proxy_addr: + # Auto-Detect local IP. + # https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + proxy_addr = s.getsockname()[0] + s.close() + + self.mode = mode + self.proxy_addr, self.proxy_port = proxy_addr, proxy_port + self.connection_cache_size = cache_size + + self.client_server_map = OrderedDict() + + self.api = APIServer(self, (api_host, api_port), APIRequestHandler) + self.api_thread = threading.Thread(target=self.api.serve_forever) + self.api_thread.daemon = True + + self.driver = WinDivert() + self.driver.register() + + self.request_filter = custom_filter or " or ".join( + ("tcp.DstPort == %d" % + p) for p in redirect_ports) + self.request_forward_handle = None + self.request_forward_thread = threading.Thread( + target=self.request_forward) + self.request_forward_thread.daemon = True + + self.addr_pid_map = dict() + self.trusted_pids = set() + self.tcptable2 = MIB_TCPTABLE2(0) + self.tcptable2_size = DWORD(0) + self.request_local_handle = None + self.request_local_thread = threading.Thread(target=self.request_local) + self.request_local_thread.daemon = True + + # The proxy server responds to the client. To the client, + # this response should look like it has been sent by the real target + self.response_filter = "outbound and tcp.SrcPort == %d" % proxy_port + self.response_handle = None + self.response_thread = threading.Thread(target=self.response) + self.response_thread.daemon = True + + self.icmp_handle = None + + @classmethod + def setup(cls): + # TODO: Make sure that server can be killed cleanly. That's a bit difficult as we don't have access to + # controller.should_exit when this is called. + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_unavailable = s.connect_ex(("127.0.0.1", PROXY_API_PORT)) + if server_unavailable: + proxifier = TransparentProxy() + proxifier.start() + + def start(self): + self.api_thread.start() + + # Block all ICMP requests (which are sent on Windows by default). + # In layman's terms: If we don't do this, our proxy machine tells the client that it can directly connect to the + # real gateway if they are on the same network. + self.icmp_handle = self.driver.open_handle( + filter="icmp", + layer=Layer.NETWORK, + flags=Flag.DROP) + + self.response_handle = self.driver.open_handle( + filter=self.response_filter, + layer=Layer.NETWORK) + self.response_thread.start() + + if self.mode == "forward" or self.mode == "both": + self.request_forward_handle = self.driver.open_handle( + filter=self.request_filter, + layer=Layer.NETWORK_FORWARD) + self.request_forward_thread.start() + if self.mode == "local" or self.mode == "both": + self.request_local_handle = self.driver.open_handle( + filter=self.request_filter, + layer=Layer.NETWORK) + self.request_local_thread.start() + + def shutdown(self): + if self.mode == "local" or self.mode == "both": + self.request_local_handle.close() + if self.mode == "forward" or self.mode == "both": + self.request_forward_handle.close() + + self.response_handle.close() + self.icmp_handle.close() + self.api.shutdown() + + def recv(self, handle): + """ + Convenience function that receives a packet from the passed handler and handles error codes. + If the process has been shut down, (None, None) is returned. + """ + try: + raw_packet, metadata = handle.recv() + return self.driver.parse_packet(raw_packet), metadata + except WindowsError as e: + if e.winerror == 995: + return None, None + else: + raise + + def fetch_pids(self): + ret = windll.iphlpapi.GetTcpTable2( + byref( + self.tcptable2), byref( + self.tcptable2_size), 0) + if ret == ERROR_INSUFFICIENT_BUFFER: + self.tcptable2 = MIB_TCPTABLE2(self.tcptable2_size.value) + self.fetch_pids() + elif ret == 0: + for row in self.tcptable2.table[:self.tcptable2.dwNumEntries]: + local = ( + socket.inet_ntoa(struct.pack('L', row.dwLocalAddr)), + socket.htons(row.dwLocalPort) + ) + self.addr_pid_map[local] = row.dwOwningPid + else: + raise RuntimeError("Unknown GetTcpTable2 return code: %s" % ret) + + def request_local(self): + while True: + packet, metadata = self.recv(self.request_local_handle) + if not packet: + return + + client = (packet.src_addr, packet.src_port) + + if client not in self.addr_pid_map: + self.fetch_pids() + + # If this fails, we most likely have a connection from an external client to + # a local server on 80/443. In this, case we always want to proxy + # the request. + pid = self.addr_pid_map.get(client, None) + + if pid not in self.trusted_pids: + self._request(packet, metadata) + else: + self.request_local_handle.send((packet.raw, metadata)) + + def request_forward(self): + """ + Redirect packages to the proxy + """ + while True: + packet, metadata = self.recv(self.request_forward_handle) + if not packet: + return + + self._request(packet, metadata) + + def _request(self, packet, metadata): + # print(" * Redirect client -> server to proxy") + # print("%s:%s -> %s:%s" % (packet.src_addr, packet.src_port, packet.dst_addr, packet.dst_port)) + client = (packet.src_addr, packet.src_port) + server = (packet.dst_addr, packet.dst_port) + + if client in self.client_server_map: + # Force re-add to mark as "newest" entry in the dict. + del self.client_server_map[client] + while len(self.client_server_map) > self.connection_cache_size: + self.client_server_map.popitem(False) + + self.client_server_map[client] = server + + packet.dst_addr, packet.dst_port = self.proxy_addr, self.proxy_port + metadata.direction = Direction.INBOUND + + packet = self.driver.update_packet_checksums(packet) + # Use any handle thats on the NETWORK layer - request_local may be + # unavailable. + self.response_handle.send((packet.raw, metadata)) + + def response(self): + """ + Spoof source address of packets send from the proxy to the client + """ + while True: + packet, metadata = self.recv(self.response_handle) + if not packet: + return + + # If the proxy responds to the client, let the client believe the target server sent the packets. + # print(" * Adjust proxy -> client") + client = (packet.dst_addr, packet.dst_port) + server = self.client_server_map.get(client, None) + if server: + packet.src_addr, packet.src_port = server + else: + print("Warning: Previously unseen connection from proxy to %s:%s." % client) + + packet = self.driver.update_packet_checksums(packet) + self.response_handle.send((packet.raw, metadata)) + + +if __name__ == "__main__": + parser = configargparse.ArgumentParser( + description="Windows Transparent Proxy") + parser.add_argument( + '--mode', + choices=[ + 'forward', + 'local', + 'both'], + default="both", + help='redirection operation mode: "forward" to only redirect forwarded packets, ' + '"local" to only redirect packets originating from the local machine') + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--redirect-ports", + nargs="+", + type=int, + default=[ + 80, + 443], + metavar="80", + help="ports that should be forwarded to the proxy") + group.add_argument( + "--custom-filter", + default=None, + metavar="WINDIVERT_FILTER", + help="Custom WinDivert interception rule.") + parser.add_argument("--proxy-addr", default=False, + help="Proxy Server Address") + parser.add_argument("--proxy-port", type=int, default=8080, + help="Proxy Server Port") + parser.add_argument("--api-host", default="localhost", + help="API hostname to bind to") + parser.add_argument("--api-port", type=int, default=PROXY_API_PORT, + help="API port") + parser.add_argument("--cache-size", type=int, default=65536, + help="Maximum connection cache size") + options = parser.parse_args() + proxy = TransparentProxy(**vars(options)) + proxy.start() + print(" * Transparent proxy active.") + print(" Filter: {0}".format(proxy.request_filter)) + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print(" * Shutting down...") + proxy.shutdown() + print(" * Shut down.") diff --git a/mitmproxy/protocol/__init__.py b/mitmproxy/protocol/__init__.py new file mode 100644 index 00000000..d44e25e9 --- /dev/null +++ b/mitmproxy/protocol/__init__.py @@ -0,0 +1,45 @@ +""" +In mitmproxy, protocols are implemented as a set of layers, which are composed on top each other. +The first layer is usually the proxy mode, e.g. transparent proxy or normal HTTP proxy. Next, +various protocol layers are stacked on top of each other - imagine WebSockets on top of an HTTP +Upgrade request. An actual mitmproxy connection may look as follows (outermost layer first): + + Transparent HTTP proxy, no TLS: + - TransparentProxy + - Http1Layer + - HttpLayer + + Regular proxy, CONNECT request with WebSockets over SSL: + - ReverseProxy + - Http1Layer + - HttpLayer + - TLSLayer + - WebsocketLayer (or TCPLayer) + +Every layer acts as a read-only context for its inner layers (see :py:class:`Layer`). To communicate +with an outer layer, a layer can use functions provided in the context. The next layer is always +determined by a call to :py:meth:`.next_layer() `, +which is provided by the root context. + +Another subtle design goal of this architecture is that upstream connections should be established +as late as possible; this makes server replay without any outgoing connections possible. +""" + +from __future__ import (absolute_import, print_function, division) +from .base import Layer, ServerConnectionMixin, Kill +from .tls import TlsLayer +from .tls import is_tls_record_magic +from .tls import TlsClientHello +from .http import UpstreamConnectLayer +from .http1 import Http1Layer +from .http2 import Http2Layer +from .rawtcp import RawTCPLayer + +__all__ = [ + "Layer", "ServerConnectionMixin", "Kill", + "TlsLayer", "is_tls_record_magic", "TlsClientHello", + "UpstreamConnectLayer", + "Http1Layer", + "Http2Layer", + "RawTCPLayer", +] diff --git a/mitmproxy/protocol/base.py b/mitmproxy/protocol/base.py new file mode 100644 index 00000000..a14066cd --- /dev/null +++ b/mitmproxy/protocol/base.py @@ -0,0 +1,198 @@ +from __future__ import (absolute_import, print_function, division) +import sys + +import six + +from ..models import ServerConnection +from ..exceptions import ProtocolException +from netlib.exceptions import TcpException + + +class _LayerCodeCompletion(object): + + """ + Dummy class that provides type hinting in PyCharm, which simplifies development a lot. + """ + + def __init__(self, **mixin_args): # pragma: no cover + super(_LayerCodeCompletion, self).__init__(**mixin_args) + if True: + return + self.config = None + """@type: mitmproxy.proxy.ProxyConfig""" + self.client_conn = None + """@type: mitmproxy.models.ClientConnection""" + self.server_conn = None + """@type: mitmproxy.models.ServerConnection""" + self.channel = None + """@type: mitmproxy.controller.Channel""" + self.ctx = None + """@type: mitmproxy.protocol.Layer""" + + +class Layer(_LayerCodeCompletion): + + """ + Base class for all layers. All other protocol layers should inherit from this class. + """ + + def __init__(self, ctx, **mixin_args): + """ + Each layer usually passes itself to its child layers as a context. Properties of the + context are transparently mapped to the layer, so that the following works: + + .. code-block:: python + + root_layer = Layer(None) + root_layer.client_conn = 42 + sub_layer = Layer(root_layer) + print(sub_layer.client_conn) # 42 + + The root layer is passed a :py:class:`mitmproxy.proxy.RootContext` object, + which provides access to :py:attr:`.client_conn `, + :py:attr:`.next_layer ` and other basic attributes. + + Args: + ctx: The (read-only) parent layer / context. + """ + self.ctx = ctx + """ + The parent layer. + + :type: :py:class:`Layer` + """ + super(Layer, self).__init__(**mixin_args) + + def __call__(self): + """Logic of the layer. + + Returns: + Once the protocol has finished without exceptions. + + Raises: + ~mitmproxy.exceptions.ProtocolException: if an exception occurs. No other exceptions must be raised. + """ + raise NotImplementedError() + + def __getattr__(self, name): + """ + Attributes not present on the current layer are looked up on the context. + """ + return getattr(self.ctx, name) + + @property + def layers(self): + """ + List of all layers, including the current layer (``[self, self.ctx, self.ctx.ctx, ...]``) + """ + return [self] + self.ctx.layers + + def __repr__(self): + return type(self).__name__ + + +class ServerConnectionMixin(object): + + """ + Mixin that provides a layer with the capabilities to manage a server connection. + The server address can be passed in the constructor or set by calling :py:meth:`set_server`. + Subclasses are responsible for calling :py:meth:`disconnect` before returning. + + Recommended Usage: + + .. code-block:: python + + class MyLayer(Layer, ServerConnectionMixin): + def __call__(self): + try: + # Do something. + finally: + if self.server_conn: + self.disconnect() + """ + + def __init__(self, server_address=None): + super(ServerConnectionMixin, self).__init__() + self.server_conn = ServerConnection(server_address, (self.config.host, 0)) + self.__check_self_connect() + + def __check_self_connect(self): + """ + We try to protect the proxy from _accidentally_ connecting to itself, + e.g. because of a failed transparent lookup or an invalid configuration. + """ + address = self.server_conn.address + if address: + self_connect = ( + address.port == self.config.port and + address.host in ("localhost", "127.0.0.1", "::1") + ) + if self_connect: + raise ProtocolException( + "Invalid server address: {}\r\n" + "The proxy shall not connect to itself.".format(repr(address)) + ) + + def set_server(self, address, server_tls=None, sni=None): + """ + Sets a new server address. If there is an existing connection, it will be closed. + + Raises: + ~mitmproxy.exceptions.ProtocolException: + if ``server_tls`` is ``True``, but there was no TLS layer on the + protocol stack which could have processed this. + """ + if self.server_conn: + self.disconnect() + self.log("Set new server address: " + repr(address), "debug") + self.server_conn.address = address + self.__check_self_connect() + if server_tls: + raise ProtocolException( + "Cannot upgrade to TLS, no TLS layer on the protocol stack." + ) + + def disconnect(self): + """ + Deletes (and closes) an existing server connection. + Must not be called if there is no existing connection. + """ + self.log("serverdisconnect", "debug", [repr(self.server_conn.address)]) + address = self.server_conn.address + source_address = self.server_conn.source_address + self.server_conn.finish() + self.server_conn.close() + self.channel.tell("serverdisconnect", self.server_conn) + self.server_conn = ServerConnection(address, source_address) + + def connect(self): + """ + Establishes a server connection. + Must not be called if there is an existing connection. + + Raises: + ~mitmproxy.exceptions.ProtocolException: if the connection could not be established. + """ + if not self.server_conn.address: + raise ProtocolException("Cannot connect to server, no server address given.") + self.log("serverconnect", "debug", [repr(self.server_conn.address)]) + self.channel.ask("serverconnect", self.server_conn) + try: + self.server_conn.connect() + except TcpException as e: + six.reraise( + ProtocolException, + ProtocolException( + "Server connection to {} failed: {}".format( + repr(self.server_conn.address), str(e) + ) + ), + sys.exc_info()[2] + ) + + +class Kill(Exception): + + """ + Signal that both client and server connection(s) should be killed immediately. + """ diff --git a/mitmproxy/protocol/http.py b/mitmproxy/protocol/http.py new file mode 100644 index 00000000..13d7903b --- /dev/null +++ b/mitmproxy/protocol/http.py @@ -0,0 +1,416 @@ +from __future__ import (absolute_import, print_function, division) + +import sys +import traceback +import six + +from netlib import tcp +from netlib.exceptions import HttpException, HttpReadDisconnect, NetlibException +from netlib.http import Headers, CONTENT_MISSING + +from h2.exceptions import H2Error + +from .. import utils +from ..exceptions import HttpProtocolException, ProtocolException +from ..models import ( + HTTPFlow, + HTTPResponse, + make_error_response, + make_connect_response, + Error, + expect_continue_response +) + +from .base import Layer, Kill + + +class _HttpTransmissionLayer(Layer): + + def read_request(self): + raise NotImplementedError() + + def read_request_body(self, request): + raise NotImplementedError() + + def send_request(self, request): + raise NotImplementedError() + + def read_response(self, request): + response = self.read_response_headers() + response.data.content = b"".join( + self.read_response_body(request, response) + ) + return response + + def read_response_headers(self): + raise NotImplementedError() + + def read_response_body(self, request, response): + raise NotImplementedError() + yield "this is a generator" # pragma: no cover + + def send_response(self, response): + if response.content == CONTENT_MISSING: + raise HttpException("Cannot assemble flow with CONTENT_MISSING") + self.send_response_headers(response) + self.send_response_body(response, [response.content]) + + def send_response_headers(self, response): + raise NotImplementedError() + + def send_response_body(self, response, chunks): + raise NotImplementedError() + + def check_close_connection(self, flow): + raise NotImplementedError() + + +class ConnectServerConnection(object): + + """ + "Fake" ServerConnection to represent state after a CONNECT request to an upstream proxy. + """ + + def __init__(self, address, ctx): + self.address = tcp.Address.wrap(address) + self._ctx = ctx + + @property + def via(self): + return self._ctx.server_conn + + def __getattr__(self, item): + return getattr(self.via, item) + + def __nonzero__(self): + return bool(self.via) + + +class UpstreamConnectLayer(Layer): + + def __init__(self, ctx, connect_request): + super(UpstreamConnectLayer, self).__init__(ctx) + self.connect_request = connect_request + self.server_conn = ConnectServerConnection( + (connect_request.host, connect_request.port), + self.ctx + ) + + def __call__(self): + layer = self.ctx.next_layer(self) + layer() + + def _send_connect_request(self): + self.send_request(self.connect_request) + resp = self.read_response(self.connect_request) + if resp.status_code != 200: + raise ProtocolException("Reconnect: Upstream server refuses CONNECT request") + + def connect(self): + if not self.server_conn: + self.ctx.connect() + self._send_connect_request() + else: + pass # swallow the message + + def change_upstream_proxy_server(self, address): + if address != self.server_conn.via.address: + self.ctx.set_server(address) + + def set_server(self, address, server_tls=None, sni=None): + if self.ctx.server_conn: + self.ctx.disconnect() + address = tcp.Address.wrap(address) + self.connect_request.host = address.host + self.connect_request.port = address.port + self.server_conn.address = address + + if server_tls: + raise ProtocolException( + "Cannot upgrade to TLS, no TLS layer on the protocol stack." + ) + + +class HttpLayer(Layer): + + def __init__(self, ctx, mode): + super(HttpLayer, self).__init__(ctx) + self.mode = mode + + self.__initial_server_conn = None + "Contains the original destination in transparent mode, which needs to be restored" + "if an inline script modified the target server for a single http request" + # We cannot rely on server_conn.tls_established, + # see https://github.com/mitmproxy/mitmproxy/issues/925 + self.__initial_server_tls = None + + def __call__(self): + if self.mode == "transparent": + self.__initial_server_tls = self._server_tls + self.__initial_server_conn = self.server_conn + while True: + try: + request = self.get_request_from_client() + self.log("request", "debug", [repr(request)]) + + # Handle Proxy Authentication + # Proxy Authentication conceptually does not work in transparent mode. + # We catch this misconfiguration on startup. Here, we sort out requests + # after a successful CONNECT request (which do not need to be validated anymore) + if self.mode != "transparent" and not self.authenticate(request): + return + + # Make sure that the incoming request matches our expectations + self.validate_request(request) + + # Regular Proxy Mode: Handle CONNECT + if self.mode == "regular" and request.form_in == "authority": + self.handle_regular_mode_connect(request) + return + + except HttpReadDisconnect: + # don't throw an error for disconnects that happen before/between requests. + return + except NetlibException as e: + self.send_error_response(400, repr(e)) + six.reraise(ProtocolException, ProtocolException( + "Error in HTTP connection: %s" % repr(e)), sys.exc_info()[2]) + + try: + flow = HTTPFlow(self.client_conn, self.server_conn, live=self) + flow.request = request + self.process_request_hook(flow) + + if not flow.response: + self.establish_server_connection(flow) + self.get_response_from_server(flow) + else: + # response was set by an inline script. + # we now need to emulate the responseheaders hook. + flow = self.channel.ask("responseheaders", flow) + if flow == Kill: + raise Kill() + + self.log("response", "debug", [repr(flow.response)]) + flow = self.channel.ask("response", flow) + if flow == Kill: + raise Kill() + self.send_response_to_client(flow) + + if self.check_close_connection(flow): + return + + # Handle 101 Switching Protocols + # It may be useful to pass additional args (such as the upgrade header) + # to next_layer in the future + if flow.response.status_code == 101: + layer = self.ctx.next_layer(self) + layer() + return + + # Upstream Proxy Mode: Handle CONNECT + if flow.request.form_in == "authority" and flow.response.status_code == 200: + self.handle_upstream_mode_connect(flow.request.copy()) + return + + except (ProtocolException, NetlibException) as e: + self.send_error_response(502, repr(e)) + + if not flow.response: + flow.error = Error(str(e)) + self.channel.ask("error", flow) + self.log(traceback.format_exc(), "debug") + return + else: + six.reraise(ProtocolException, ProtocolException( + "Error in HTTP connection: %s" % repr(e)), sys.exc_info()[2]) + finally: + flow.live = False + + def get_request_from_client(self): + request = self.read_request() + if request.headers.get("expect", "").lower() == "100-continue": + # TODO: We may have to use send_response_headers for HTTP2 here. + self.send_response(expect_continue_response) + request.headers.pop("expect") + request.body = b"".join(self.read_request_body(request)) + return request + + def send_error_response(self, code, message): + try: + response = make_error_response(code, message) + self.send_response(response) + except (NetlibException, H2Error): + self.log(traceback.format_exc(), "debug") + + def change_upstream_proxy_server(self, address): + # Make set_upstream_proxy_server always available, + # even if there's no UpstreamConnectLayer + if address != self.server_conn.address: + return self.set_server(address) + + def handle_regular_mode_connect(self, request): + self.set_server((request.host, request.port)) + self.send_response(make_connect_response(request.http_version)) + layer = self.ctx.next_layer(self) + layer() + + def handle_upstream_mode_connect(self, connect_request): + layer = UpstreamConnectLayer(self, connect_request) + layer() + + def send_response_to_client(self, flow): + if not flow.response.stream: + # no streaming: + # we already received the full response from the server and can + # send it to the client straight away. + self.send_response(flow.response) + else: + # streaming: + # First send the headers and then transfer the response incrementally + self.send_response_headers(flow.response) + chunks = self.read_response_body( + flow.request, + flow.response + ) + if callable(flow.response.stream): + chunks = flow.response.stream(chunks) + self.send_response_body(flow.response, chunks) + flow.response.timestamp_end = utils.timestamp() + + def get_response_from_server(self, flow): + def get_response(): + self.send_request(flow.request) + flow.response = self.read_response_headers() + + try: + get_response() + except NetlibException as v: + self.log( + "server communication error: %s" % repr(v), + level="debug" + ) + # In any case, we try to reconnect at least once. This is + # necessary because it might be possible that we already + # initiated an upstream connection after clientconnect that + # has already been expired, e.g consider the following event + # log: + # > clientconnect (transparent mode destination known) + # > serverconnect (required for client tls handshake) + # > read n% of large request + # > server detects timeout, disconnects + # > read (100-n)% of large request + # > send large request upstream + self.disconnect() + self.connect() + get_response() + + # call the appropriate script hook - this is an opportunity for an + # inline script to set flow.stream = True + flow = self.channel.ask("responseheaders", flow) + if flow == Kill: + raise Kill() + + if flow.response.stream: + flow.response.data.content = CONTENT_MISSING + else: + flow.response.data.content = b"".join(self.read_response_body( + flow.request, + flow.response + )) + flow.response.timestamp_end = utils.timestamp() + + # no further manipulation of self.server_conn beyond this point + # we can safely set it as the final attribute value here. + flow.server_conn = self.server_conn + + def process_request_hook(self, flow): + # Determine .scheme, .host and .port attributes for inline scripts. + # For absolute-form requests, they are directly given in the request. + # For authority-form requests, we only need to determine the request scheme. + # For relative-form requests, we need to determine host and port as + # well. + if self.mode == "regular": + pass # only absolute-form at this point, nothing to do here. + elif self.mode == "upstream": + if flow.request.form_in == "authority": + flow.request.scheme = "http" # pseudo value + else: + # Setting request.host also updates the host header, which we want to preserve + host_header = flow.request.headers.get("host", None) + flow.request.host = self.__initial_server_conn.address.host + flow.request.port = self.__initial_server_conn.address.port + if host_header: + flow.request.headers["host"] = host_header + flow.request.scheme = "https" if self.__initial_server_tls else "http" + + request_reply = self.channel.ask("request", flow) + if request_reply == Kill: + raise Kill() + if isinstance(request_reply, HTTPResponse): + flow.response = request_reply + return + + def establish_server_connection(self, flow): + address = tcp.Address((flow.request.host, flow.request.port)) + tls = (flow.request.scheme == "https") + + if self.mode == "regular" or self.mode == "transparent": + # If there's an existing connection that doesn't match our expectations, kill it. + if address != self.server_conn.address or tls != self.server_conn.tls_established: + self.set_server(address, tls, address.host) + # Establish connection is neccessary. + if not self.server_conn: + self.connect() + else: + if not self.server_conn: + self.connect() + if tls: + raise HttpProtocolException("Cannot change scheme in upstream proxy mode.") + """ + # This is a very ugly (untested) workaround to solve a very ugly problem. + if self.server_conn and self.server_conn.tls_established and not ssl: + self.disconnect() + self.connect() + elif ssl and not hasattr(self, "connected_to") or self.connected_to != address: + if self.server_conn.tls_established: + self.disconnect() + self.connect() + + self.send_request(make_connect_request(address)) + tls_layer = TlsLayer(self, False, True) + tls_layer._establish_tls_with_server() + """ + + def validate_request(self, request): + if request.form_in == "absolute" and request.scheme != "http": + raise HttpException("Invalid request scheme: %s" % request.scheme) + + expected_request_forms = { + "regular": ("authority", "absolute",), + "upstream": ("authority", "absolute"), + "transparent": ("relative",) + } + + allowed_request_forms = expected_request_forms[self.mode] + if request.form_in not in allowed_request_forms: + err_message = "Invalid HTTP request form (expected: %s, got: %s)" % ( + " or ".join(allowed_request_forms), request.form_in + ) + raise HttpException(err_message) + + if self.mode == "regular" and request.form_in == "absolute": + request.form_out = "relative" + + def authenticate(self, request): + if self.config.authenticator: + if self.config.authenticator.authenticate(request.headers): + self.config.authenticator.clean(request.headers) + else: + self.send_response(make_error_response( + 407, + "Proxy Authentication Required", + Headers(**self.config.authenticator.auth_challenge_headers()) + )) + return False + return True diff --git a/mitmproxy/protocol/http1.py b/mitmproxy/protocol/http1.py new file mode 100644 index 00000000..a4cd8801 --- /dev/null +++ b/mitmproxy/protocol/http1.py @@ -0,0 +1,67 @@ +from __future__ import (absolute_import, print_function, division) + + +from netlib.http import http1 + +from .http import _HttpTransmissionLayer, HttpLayer +from ..models import HTTPRequest, HTTPResponse + + +class Http1Layer(_HttpTransmissionLayer): + + def __init__(self, ctx, mode): + super(Http1Layer, self).__init__(ctx) + self.mode = mode + + def read_request(self): + req = http1.read_request(self.client_conn.rfile, body_size_limit=self.config.body_size_limit) + return HTTPRequest.wrap(req) + + def read_request_body(self, request): + expected_size = http1.expected_http_body_size(request) + return http1.read_body(self.client_conn.rfile, expected_size, self.config.body_size_limit) + + def send_request(self, request): + self.server_conn.wfile.write(http1.assemble_request(request)) + self.server_conn.wfile.flush() + + def read_response_headers(self): + resp = http1.read_response_head(self.server_conn.rfile) + return HTTPResponse.wrap(resp) + + def read_response_body(self, request, response): + expected_size = http1.expected_http_body_size(request, response) + return http1.read_body(self.server_conn.rfile, expected_size, self.config.body_size_limit) + + def send_response_headers(self, response): + raw = http1.assemble_response_head(response) + self.client_conn.wfile.write(raw) + self.client_conn.wfile.flush() + + def send_response_body(self, response, chunks): + for chunk in http1.assemble_body(response.headers, chunks): + self.client_conn.wfile.write(chunk) + self.client_conn.wfile.flush() + + def check_close_connection(self, flow): + request_close = http1.connection_close( + flow.request.http_version, + flow.request.headers + ) + response_close = http1.connection_close( + flow.response.http_version, + flow.response.headers + ) + read_until_eof = http1.expected_http_body_size(flow.request, flow.response) == -1 + close_connection = request_close or response_close or read_until_eof + if flow.request.form_in == "authority" and flow.response.status_code == 200: + # Workaround for https://github.com/mitmproxy/mitmproxy/issues/313: + # Charles Proxy sends a CONNECT response with HTTP/1.0 + # and no Content-Length header + + return False + return close_connection + + def __call__(self): + layer = HttpLayer(self, self.mode) + layer() diff --git a/mitmproxy/protocol/http2.py b/mitmproxy/protocol/http2.py new file mode 100644 index 00000000..3fbc500a --- /dev/null +++ b/mitmproxy/protocol/http2.py @@ -0,0 +1,422 @@ +from __future__ import (absolute_import, print_function, division) + +import threading +import time +import Queue + +from netlib.tcp import ssl_read_select +from netlib.exceptions import HttpException +from netlib.http import Headers +from netlib.utils import http2_read_raw_frame + +from h2.connection import H2Connection +from h2.events import * + +from .base import Layer +from .http import _HttpTransmissionLayer, HttpLayer +from .. import utils +from ..models import HTTPRequest, HTTPResponse + + +class SafeH2Connection(H2Connection): + + def __init__(self, conn, *args, **kwargs): + super(SafeH2Connection, self).__init__(*args, **kwargs) + self.conn = conn + self.lock = threading.RLock() + + def safe_close_connection(self, error_code): + with self.lock: + self.close_connection(error_code) + self.conn.send(self.data_to_send()) + + def safe_increment_flow_control(self, stream_id, length): + if length == 0: + return + + with self.lock: + self.increment_flow_control_window(length) + self.conn.send(self.data_to_send()) + with self.lock: + if stream_id in self.streams and not self.streams[stream_id].closed: + self.increment_flow_control_window(length, stream_id=stream_id) + self.conn.send(self.data_to_send()) + + def safe_reset_stream(self, stream_id, error_code): + with self.lock: + try: + self.reset_stream(stream_id, error_code) + except h2.exceptions.StreamClosedError: + # stream is already closed - good + pass + self.conn.send(self.data_to_send()) + + def safe_update_settings(self, new_settings): + with self.lock: + self.update_settings(new_settings) + self.conn.send(self.data_to_send()) + + def safe_send_headers(self, is_zombie, stream_id, headers): + with self.lock: + if is_zombie(): + return + self.send_headers(stream_id, headers) + self.conn.send(self.data_to_send()) + + def safe_send_body(self, is_zombie, stream_id, chunks): + for chunk in chunks: + position = 0 + while position < len(chunk): + self.lock.acquire() + if is_zombie(): + self.lock.release() + return + max_outbound_frame_size = self.max_outbound_frame_size + frame_chunk = chunk[position:position + max_outbound_frame_size] + if self.local_flow_control_window(stream_id) < len(frame_chunk): + self.lock.release() + time.sleep(0) + continue + self.send_data(stream_id, frame_chunk) + self.conn.send(self.data_to_send()) + self.lock.release() + position += max_outbound_frame_size + with self.lock: + if is_zombie(): + return + self.end_stream(stream_id) + self.conn.send(self.data_to_send()) + + +class Http2Layer(Layer): + + def __init__(self, ctx, mode): + super(Http2Layer, self).__init__(ctx) + self.mode = mode + self.streams = dict() + self.client_reset_streams = [] + self.server_reset_streams = [] + self.server_to_client_stream_ids = dict([(0, 0)]) + self.client_conn.h2 = SafeH2Connection(self.client_conn, client_side=False) + + # make sure that we only pass actual SSL.Connection objects in here, + # because otherwise ssl_read_select fails! + self.active_conns = [self.client_conn.connection] + + def _initiate_server_conn(self): + self.server_conn.h2 = SafeH2Connection(self.server_conn, client_side=True) + self.server_conn.h2.initiate_connection() + self.server_conn.send(self.server_conn.h2.data_to_send()) + self.active_conns.append(self.server_conn.connection) + + def connect(self): # pragma: no cover + raise ValueError("CONNECT inside an HTTP2 stream is not supported.") + # self.ctx.connect() + # self.server_conn.connect() + # self._initiate_server_conn() + + def set_server(self): # pragma: no cover + raise NotImplementedError("Cannot change server for HTTP2 connections.") + + def disconnect(self): # pragma: no cover + raise NotImplementedError("Cannot dis- or reconnect in HTTP2 connections.") + + def next_layer(self): # pragma: no cover + # WebSockets over HTTP/2? + # CONNECT for proxying? + raise NotImplementedError() + + def _handle_event(self, event, source_conn, other_conn, is_server): + self.log( + "HTTP2 Event from {}".format("server" if is_server else "client"), + "debug", + [repr(event)] + ) + + if hasattr(event, 'stream_id'): + if is_server and event.stream_id % 2 == 1: + eid = self.server_to_client_stream_ids[event.stream_id] + else: + eid = event.stream_id + + if isinstance(event, RequestReceived): + headers = Headers([[str(k), str(v)] for k, v in event.headers]) + self.streams[eid] = Http2SingleStreamLayer(self, eid, headers) + self.streams[eid].timestamp_start = time.time() + self.streams[eid].start() + elif isinstance(event, ResponseReceived): + headers = Headers([[str(k), str(v)] for k, v in event.headers]) + self.streams[eid].queued_data_length = 0 + self.streams[eid].timestamp_start = time.time() + self.streams[eid].response_headers = headers + self.streams[eid].response_arrived.set() + elif isinstance(event, DataReceived): + if self.config.body_size_limit and self.streams[eid].queued_data_length > self.config.body_size_limit: + raise HttpException("HTTP body too large. Limit is {}.".format(self.config.body_size_limit)) + self.streams[eid].data_queue.put(event.data) + self.streams[eid].queued_data_length += len(event.data) + source_conn.h2.safe_increment_flow_control(event.stream_id, event.flow_controlled_length) + elif isinstance(event, StreamEnded): + self.streams[eid].timestamp_end = time.time() + self.streams[eid].data_finished.set() + elif isinstance(event, StreamReset): + self.streams[eid].zombie = time.time() + self.client_reset_streams.append(self.streams[eid].client_stream_id) + if self.streams[eid].server_stream_id: + self.server_reset_streams.append(self.streams[eid].server_stream_id) + if eid in self.streams and event.error_code == 0x8: + if is_server: + other_stream_id = self.streams[eid].client_stream_id + else: + other_stream_id = self.streams[eid].server_stream_id + if other_stream_id is not None: + other_conn.h2.safe_reset_stream(other_stream_id, event.error_code) + elif isinstance(event, RemoteSettingsChanged): + new_settings = dict([(id, cs.new_value) for (id, cs) in event.changed_settings.iteritems()]) + other_conn.h2.safe_update_settings(new_settings) + elif isinstance(event, ConnectionTerminated): + # Do not immediately terminate the other connection. + # Some streams might be still sending data to the client. + return False + elif isinstance(event, PushedStreamReceived): + # pushed stream ids should be uniq and not dependent on race conditions + # only the parent stream id must be looked up first + parent_eid = self.server_to_client_stream_ids[event.parent_stream_id] + with self.client_conn.h2.lock: + self.client_conn.h2.push_stream(parent_eid, event.pushed_stream_id, event.headers) + + headers = Headers([[str(k), str(v)] for k, v in event.headers]) + headers['x-mitmproxy-pushed'] = 'true' + self.streams[event.pushed_stream_id] = Http2SingleStreamLayer(self, event.pushed_stream_id, headers) + self.streams[event.pushed_stream_id].timestamp_start = time.time() + self.streams[event.pushed_stream_id].pushed = True + self.streams[event.pushed_stream_id].parent_stream_id = parent_eid + self.streams[event.pushed_stream_id].timestamp_end = time.time() + self.streams[event.pushed_stream_id].request_data_finished.set() + self.streams[event.pushed_stream_id].start() + elif isinstance(event, TrailersReceived): + raise NotImplementedError() + + return True + + def _cleanup_streams(self): + death_time = time.time() - 10 + for stream_id in self.streams.keys(): + zombie = self.streams[stream_id].zombie + if zombie and zombie <= death_time: + self.streams.pop(stream_id, None) + + def __call__(self): + if self.server_conn: + self._initiate_server_conn() + + preamble = self.client_conn.rfile.read(24) + self.client_conn.h2.initiate_connection() + self.client_conn.h2.receive_data(preamble) + self.client_conn.send(self.client_conn.h2.data_to_send()) + + while True: + r = ssl_read_select(self.active_conns, 1) + for conn in r: + source_conn = self.client_conn if conn == self.client_conn.connection else self.server_conn + other_conn = self.server_conn if conn == self.client_conn.connection else self.client_conn + is_server = (conn == self.server_conn.connection) + + with source_conn.h2.lock: + try: + raw_frame = b''.join(http2_read_raw_frame(source_conn.rfile)) + except: + for stream in self.streams.values(): + stream.zombie = time.time() + return + + events = source_conn.h2.receive_data(raw_frame) + source_conn.send(source_conn.h2.data_to_send()) + + for event in events: + if not self._handle_event(event, source_conn, other_conn, is_server): + return + + self._cleanup_streams() + + +class Http2SingleStreamLayer(_HttpTransmissionLayer, threading.Thread): + + def __init__(self, ctx, stream_id, request_headers): + super(Http2SingleStreamLayer, self).__init__(ctx) + self.zombie = None + self.client_stream_id = stream_id + self.server_stream_id = None + self.request_headers = request_headers + self.response_headers = None + self.pushed = False + + self.request_data_queue = Queue.Queue() + self.request_queued_data_length = 0 + self.request_data_finished = threading.Event() + + self.response_arrived = threading.Event() + self.response_data_queue = Queue.Queue() + self.response_queued_data_length = 0 + self.response_data_finished = threading.Event() + + @property + def data_queue(self): + if self.response_arrived.is_set(): + return self.response_data_queue + else: + return self.request_data_queue + + @property + def queued_data_length(self): + if self.response_arrived.is_set(): + return self.response_queued_data_length + else: + return self.request_queued_data_length + + @property + def data_finished(self): + if self.response_arrived.is_set(): + return self.response_data_finished + else: + return self.request_data_finished + + @queued_data_length.setter + def queued_data_length(self, v): + if self.response_arrived.is_set(): + return self.response_queued_data_length + else: + return self.request_queued_data_length + + def is_zombie(self): + return self.zombie is not None + + def read_request(self): + self.request_data_finished.wait() + + authority = self.request_headers.get(':authority', '') + method = self.request_headers.get(':method', 'GET') + scheme = self.request_headers.get(':scheme', 'https') + path = self.request_headers.get(':path', '/') + host = None + port = None + + if path == '*' or path.startswith("/"): + form_in = "relative" + elif method == 'CONNECT': # pragma: no cover + raise NotImplementedError("CONNECT over HTTP/2 is not implemented.") + else: # pragma: no cover + form_in = "absolute" + # FIXME: verify if path or :host contains what we need + scheme, host, port, _ = utils.parse_url(path) + + if authority: + host, _, port = authority.partition(':') + + if not host: + host = 'localhost' + if not port: + port = 443 if scheme == 'https' else 80 + port = int(port) + + data = [] + while self.request_data_queue.qsize() > 0: + data.append(self.request_data_queue.get()) + data = b"".join(data) + + return HTTPRequest( + form_in, + method, + scheme, + host, + port, + path, + b"HTTP/2.0", + self.request_headers, + data, + timestamp_start=self.timestamp_start, + timestamp_end=self.timestamp_end, + ) + + def send_request(self, message): + if self.pushed: + # nothing to do here + return + + with self.server_conn.h2.lock: + # We must not assign a stream id if we are already a zombie. + if self.zombie: + return + + self.server_stream_id = self.server_conn.h2.get_next_available_stream_id() + self.server_to_client_stream_ids[self.server_stream_id] = self.client_stream_id + + self.server_conn.h2.safe_send_headers( + self.is_zombie, + self.server_stream_id, + message.headers + ) + self.server_conn.h2.safe_send_body( + self.is_zombie, + self.server_stream_id, + message.body + ) + + def read_response_headers(self): + self.response_arrived.wait() + + status_code = int(self.response_headers.get(':status', 502)) + + return HTTPResponse( + http_version=b"HTTP/2.0", + status_code=status_code, + reason='', + headers=self.response_headers, + content=None, + timestamp_start=self.timestamp_start, + timestamp_end=self.timestamp_end, + ) + + def read_response_body(self, request, response): + while True: + try: + yield self.response_data_queue.get(timeout=1) + except Queue.Empty: + pass + if self.response_data_finished.is_set(): + while self.response_data_queue.qsize() > 0: + yield self.response_data_queue.get() + return + if self.zombie: + return + + def send_response_headers(self, response): + self.client_conn.h2.safe_send_headers( + self.is_zombie, + self.client_stream_id, + response.headers + ) + + def send_response_body(self, _response, chunks): + self.client_conn.h2.safe_send_body( + self.is_zombie, + self.client_stream_id, + chunks + ) + + def check_close_connection(self, flow): + # This layer only handles a single stream. + # RFC 7540 8.1: An HTTP request/response exchange fully consumes a single stream. + return True + + def connect(self): # pragma: no cover + raise ValueError("CONNECT inside an HTTP2 stream is not supported.") + + def set_server(self, *args, **kwargs): # pragma: no cover + # do not mess with the server connection - all streams share it. + pass + + def run(self): + layer = HttpLayer(self, self.mode) + layer() + self.zombie = time.time() diff --git a/mitmproxy/protocol/http_replay.py b/mitmproxy/protocol/http_replay.py new file mode 100644 index 00000000..6316f26c --- /dev/null +++ b/mitmproxy/protocol/http_replay.py @@ -0,0 +1,105 @@ +from __future__ import (absolute_import, print_function, division) +import threading +import traceback +from mitmproxy.exceptions import ReplayException +from netlib.exceptions import HttpException, TcpException +from netlib.http import http1 + +from ..controller import Channel +from ..models import Error, HTTPResponse, ServerConnection, make_connect_request +from .base import Kill + + +# TODO: Doesn't really belong into mitmproxy.protocol... + + +class RequestReplayThread(threading.Thread): + name = "RequestReplayThread" + + def __init__(self, config, flow, masterq, should_exit): + """ + masterqueue can be a queue or None, if no scripthooks should be + processed. + """ + self.config, self.flow = config, flow + if masterq: + self.channel = Channel(masterq, should_exit) + else: + self.channel = None + super(RequestReplayThread, self).__init__() + + def run(self): + r = self.flow.request + form_out_backup = r.form_out + try: + self.flow.response = None + + # If we have a channel, run script hooks. + if self.channel: + request_reply = self.channel.ask("request", self.flow) + if request_reply == Kill: + raise Kill() + elif isinstance(request_reply, HTTPResponse): + self.flow.response = request_reply + + if not self.flow.response: + # In all modes, we directly connect to the server displayed + if self.config.mode == "upstream": + server_address = self.config.upstream_server.address + server = ServerConnection(server_address, (self.config.host, 0)) + server.connect() + if r.scheme == "https": + connect_request = make_connect_request((r.host, r.port)) + server.wfile.write(http1.assemble_request(connect_request)) + server.wfile.flush() + resp = http1.read_response( + server.rfile, + connect_request, + body_size_limit=self.config.body_size_limit + ) + if resp.status_code != 200: + raise ReplayException("Upstream server refuses CONNECT request") + server.establish_ssl( + self.config.clientcerts, + sni=self.flow.server_conn.sni + ) + r.form_out = "relative" + else: + r.form_out = "absolute" + else: + server_address = (r.host, r.port) + server = ServerConnection(server_address, (self.config.host, 0)) + server.connect() + if r.scheme == "https": + server.establish_ssl( + self.config.clientcerts, + sni=self.flow.server_conn.sni + ) + r.form_out = "relative" + + server.wfile.write(http1.assemble_request(r)) + server.wfile.flush() + self.flow.server_conn = server + self.flow.response = HTTPResponse.wrap(http1.read_response( + server.rfile, + r, + body_size_limit=self.config.body_size_limit + )) + if self.channel: + response_reply = self.channel.ask("response", self.flow) + if response_reply == Kill: + raise Kill() + except (ReplayException, HttpException, TcpException) as e: + self.flow.error = Error(str(e)) + if self.channel: + self.channel.ask("error", self.flow) + except Kill: + # Kill should only be raised if there's a channel in the + # first place. + from ..proxy.root_context import Log + self.channel.tell("log", Log("Connection killed", "info")) + except Exception: + from ..proxy.root_context import Log + self.channel.tell("log", Log(traceback.format_exc(), "error")) + finally: + r.form_out = form_out_backup diff --git a/mitmproxy/protocol/rawtcp.py b/mitmproxy/protocol/rawtcp.py new file mode 100644 index 00000000..b87899e4 --- /dev/null +++ b/mitmproxy/protocol/rawtcp.py @@ -0,0 +1,88 @@ +from __future__ import (absolute_import, print_function, division) +import socket +import six +import sys + +from OpenSSL import SSL +from netlib.exceptions import TcpException + +from netlib.tcp import ssl_read_select +from netlib.utils import clean_bin +from ..exceptions import ProtocolException +from .base import Layer + + +class TcpMessage(object): + + def __init__(self, client_conn, server_conn, sender, receiver, message): + self.client_conn = client_conn + self.server_conn = server_conn + self.sender = sender + self.receiver = receiver + self.message = message + + +class RawTCPLayer(Layer): + chunk_size = 4096 + + def __init__(self, ctx, logging=True): + self.logging = logging + super(RawTCPLayer, self).__init__(ctx) + + def __call__(self): + self.connect() + + buf = memoryview(bytearray(self.chunk_size)) + + client = self.client_conn.connection + server = self.server_conn.connection + conns = [client, server] + + try: + while True: + r = ssl_read_select(conns, 10) + for conn in r: + dst = server if conn == client else client + + size = conn.recv_into(buf, self.chunk_size) + if not size: + conns.remove(conn) + # Shutdown connection to the other peer + if isinstance(conn, SSL.Connection): + # We can't half-close a connection, so we just close everything here. + # Sockets will be cleaned up on a higher level. + return + else: + dst.shutdown(socket.SHUT_WR) + + if len(conns) == 0: + return + continue + + tcp_message = TcpMessage( + self.client_conn, self.server_conn, + self.client_conn if dst == server else self.server_conn, + self.server_conn if dst == server else self.client_conn, + buf[:size].tobytes()) + self.channel.ask("tcp_message", tcp_message) + dst.sendall(tcp_message.message) + + if self.logging: + # log messages are prepended with the client address, + # hence the "weird" direction string. + if dst == server: + direction = "-> tcp -> {}".format(repr(self.server_conn.address)) + else: + direction = "<- tcp <- {}".format(repr(self.server_conn.address)) + data = clean_bin(tcp_message.message) + self.log( + "{}\r\n{}".format(direction, data), + "info" + ) + + except (socket.error, TcpException, SSL.Error) as e: + six.reraise( + ProtocolException, + ProtocolException("TCP connection closed unexpectedly: {}".format(repr(e))), + sys.exc_info()[2] + ) diff --git a/mitmproxy/protocol/tls.py b/mitmproxy/protocol/tls.py new file mode 100644 index 00000000..6913396d --- /dev/null +++ b/mitmproxy/protocol/tls.py @@ -0,0 +1,566 @@ +from __future__ import (absolute_import, print_function, division) + +import struct +import sys + +from construct import ConstructError +import six +from netlib.exceptions import InvalidCertificateException +from netlib.exceptions import TlsException + +from ..contrib.tls._constructs import ClientHello +from ..exceptions import ProtocolException, TlsProtocolException, ClientHandshakeException +from .base import Layer + + +# taken from https://testssl.sh/openssl-rfc.mappping.html +CIPHER_ID_NAME_MAP = { + 0x00: 'NULL-MD5', + 0x01: 'NULL-MD5', + 0x02: 'NULL-SHA', + 0x03: 'EXP-RC4-MD5', + 0x04: 'RC4-MD5', + 0x05: 'RC4-SHA', + 0x06: 'EXP-RC2-CBC-MD5', + 0x07: 'IDEA-CBC-SHA', + 0x08: 'EXP-DES-CBC-SHA', + 0x09: 'DES-CBC-SHA', + 0x0a: 'DES-CBC3-SHA', + 0x0b: 'EXP-DH-DSS-DES-CBC-SHA', + 0x0c: 'DH-DSS-DES-CBC-SHA', + 0x0d: 'DH-DSS-DES-CBC3-SHA', + 0x0e: 'EXP-DH-RSA-DES-CBC-SHA', + 0x0f: 'DH-RSA-DES-CBC-SHA', + 0x10: 'DH-RSA-DES-CBC3-SHA', + 0x11: 'EXP-EDH-DSS-DES-CBC-SHA', + 0x12: 'EDH-DSS-DES-CBC-SHA', + 0x13: 'EDH-DSS-DES-CBC3-SHA', + 0x14: 'EXP-EDH-RSA-DES-CBC-SHA', + 0x15: 'EDH-RSA-DES-CBC-SHA', + 0x16: 'EDH-RSA-DES-CBC3-SHA', + 0x17: 'EXP-ADH-RC4-MD5', + 0x18: 'ADH-RC4-MD5', + 0x19: 'EXP-ADH-DES-CBC-SHA', + 0x1a: 'ADH-DES-CBC-SHA', + 0x1b: 'ADH-DES-CBC3-SHA', + # 0x1c: , + # 0x1d: , + 0x1e: 'KRB5-DES-CBC-SHA', + 0x1f: 'KRB5-DES-CBC3-SHA', + 0x20: 'KRB5-RC4-SHA', + 0x21: 'KRB5-IDEA-CBC-SHA', + 0x22: 'KRB5-DES-CBC-MD5', + 0x23: 'KRB5-DES-CBC3-MD5', + 0x24: 'KRB5-RC4-MD5', + 0x25: 'KRB5-IDEA-CBC-MD5', + 0x26: 'EXP-KRB5-DES-CBC-SHA', + 0x27: 'EXP-KRB5-RC2-CBC-SHA', + 0x28: 'EXP-KRB5-RC4-SHA', + 0x29: 'EXP-KRB5-DES-CBC-MD5', + 0x2a: 'EXP-KRB5-RC2-CBC-MD5', + 0x2b: 'EXP-KRB5-RC4-MD5', + 0x2f: 'AES128-SHA', + 0x30: 'DH-DSS-AES128-SHA', + 0x31: 'DH-RSA-AES128-SHA', + 0x32: 'DHE-DSS-AES128-SHA', + 0x33: 'DHE-RSA-AES128-SHA', + 0x34: 'ADH-AES128-SHA', + 0x35: 'AES256-SHA', + 0x36: 'DH-DSS-AES256-SHA', + 0x37: 'DH-RSA-AES256-SHA', + 0x38: 'DHE-DSS-AES256-SHA', + 0x39: 'DHE-RSA-AES256-SHA', + 0x3a: 'ADH-AES256-SHA', + 0x3b: 'NULL-SHA256', + 0x3c: 'AES128-SHA256', + 0x3d: 'AES256-SHA256', + 0x3e: 'DH-DSS-AES128-SHA256', + 0x3f: 'DH-RSA-AES128-SHA256', + 0x40: 'DHE-DSS-AES128-SHA256', + 0x41: 'CAMELLIA128-SHA', + 0x42: 'DH-DSS-CAMELLIA128-SHA', + 0x43: 'DH-RSA-CAMELLIA128-SHA', + 0x44: 'DHE-DSS-CAMELLIA128-SHA', + 0x45: 'DHE-RSA-CAMELLIA128-SHA', + 0x46: 'ADH-CAMELLIA128-SHA', + 0x62: 'EXP1024-DES-CBC-SHA', + 0x63: 'EXP1024-DHE-DSS-DES-CBC-SHA', + 0x64: 'EXP1024-RC4-SHA', + 0x65: 'EXP1024-DHE-DSS-RC4-SHA', + 0x66: 'DHE-DSS-RC4-SHA', + 0x67: 'DHE-RSA-AES128-SHA256', + 0x68: 'DH-DSS-AES256-SHA256', + 0x69: 'DH-RSA-AES256-SHA256', + 0x6a: 'DHE-DSS-AES256-SHA256', + 0x6b: 'DHE-RSA-AES256-SHA256', + 0x6c: 'ADH-AES128-SHA256', + 0x6d: 'ADH-AES256-SHA256', + 0x80: 'GOST94-GOST89-GOST89', + 0x81: 'GOST2001-GOST89-GOST89', + 0x82: 'GOST94-NULL-GOST94', + 0x83: 'GOST2001-GOST89-GOST89', + 0x84: 'CAMELLIA256-SHA', + 0x85: 'DH-DSS-CAMELLIA256-SHA', + 0x86: 'DH-RSA-CAMELLIA256-SHA', + 0x87: 'DHE-DSS-CAMELLIA256-SHA', + 0x88: 'DHE-RSA-CAMELLIA256-SHA', + 0x89: 'ADH-CAMELLIA256-SHA', + 0x8a: 'PSK-RC4-SHA', + 0x8b: 'PSK-3DES-EDE-CBC-SHA', + 0x8c: 'PSK-AES128-CBC-SHA', + 0x8d: 'PSK-AES256-CBC-SHA', + # 0x8e: , + # 0x8f: , + # 0x90: , + # 0x91: , + # 0x92: , + # 0x93: , + # 0x94: , + # 0x95: , + 0x96: 'SEED-SHA', + 0x97: 'DH-DSS-SEED-SHA', + 0x98: 'DH-RSA-SEED-SHA', + 0x99: 'DHE-DSS-SEED-SHA', + 0x9a: 'DHE-RSA-SEED-SHA', + 0x9b: 'ADH-SEED-SHA', + 0x9c: 'AES128-GCM-SHA256', + 0x9d: 'AES256-GCM-SHA384', + 0x9e: 'DHE-RSA-AES128-GCM-SHA256', + 0x9f: 'DHE-RSA-AES256-GCM-SHA384', + 0xa0: 'DH-RSA-AES128-GCM-SHA256', + 0xa1: 'DH-RSA-AES256-GCM-SHA384', + 0xa2: 'DHE-DSS-AES128-GCM-SHA256', + 0xa3: 'DHE-DSS-AES256-GCM-SHA384', + 0xa4: 'DH-DSS-AES128-GCM-SHA256', + 0xa5: 'DH-DSS-AES256-GCM-SHA384', + 0xa6: 'ADH-AES128-GCM-SHA256', + 0xa7: 'ADH-AES256-GCM-SHA384', + 0x5600: 'TLS_FALLBACK_SCSV', + 0xc001: 'ECDH-ECDSA-NULL-SHA', + 0xc002: 'ECDH-ECDSA-RC4-SHA', + 0xc003: 'ECDH-ECDSA-DES-CBC3-SHA', + 0xc004: 'ECDH-ECDSA-AES128-SHA', + 0xc005: 'ECDH-ECDSA-AES256-SHA', + 0xc006: 'ECDHE-ECDSA-NULL-SHA', + 0xc007: 'ECDHE-ECDSA-RC4-SHA', + 0xc008: 'ECDHE-ECDSA-DES-CBC3-SHA', + 0xc009: 'ECDHE-ECDSA-AES128-SHA', + 0xc00a: 'ECDHE-ECDSA-AES256-SHA', + 0xc00b: 'ECDH-RSA-NULL-SHA', + 0xc00c: 'ECDH-RSA-RC4-SHA', + 0xc00d: 'ECDH-RSA-DES-CBC3-SHA', + 0xc00e: 'ECDH-RSA-AES128-SHA', + 0xc00f: 'ECDH-RSA-AES256-SHA', + 0xc010: 'ECDHE-RSA-NULL-SHA', + 0xc011: 'ECDHE-RSA-RC4-SHA', + 0xc012: 'ECDHE-RSA-DES-CBC3-SHA', + 0xc013: 'ECDHE-RSA-AES128-SHA', + 0xc014: 'ECDHE-RSA-AES256-SHA', + 0xc015: 'AECDH-NULL-SHA', + 0xc016: 'AECDH-RC4-SHA', + 0xc017: 'AECDH-DES-CBC3-SHA', + 0xc018: 'AECDH-AES128-SHA', + 0xc019: 'AECDH-AES256-SHA', + 0xc01a: 'SRP-3DES-EDE-CBC-SHA', + 0xc01b: 'SRP-RSA-3DES-EDE-CBC-SHA', + 0xc01c: 'SRP-DSS-3DES-EDE-CBC-SHA', + 0xc01d: 'SRP-AES-128-CBC-SHA', + 0xc01e: 'SRP-RSA-AES-128-CBC-SHA', + 0xc01f: 'SRP-DSS-AES-128-CBC-SHA', + 0xc020: 'SRP-AES-256-CBC-SHA', + 0xc021: 'SRP-RSA-AES-256-CBC-SHA', + 0xc022: 'SRP-DSS-AES-256-CBC-SHA', + 0xc023: 'ECDHE-ECDSA-AES128-SHA256', + 0xc024: 'ECDHE-ECDSA-AES256-SHA384', + 0xc025: 'ECDH-ECDSA-AES128-SHA256', + 0xc026: 'ECDH-ECDSA-AES256-SHA384', + 0xc027: 'ECDHE-RSA-AES128-SHA256', + 0xc028: 'ECDHE-RSA-AES256-SHA384', + 0xc029: 'ECDH-RSA-AES128-SHA256', + 0xc02a: 'ECDH-RSA-AES256-SHA384', + 0xc02b: 'ECDHE-ECDSA-AES128-GCM-SHA256', + 0xc02c: 'ECDHE-ECDSA-AES256-GCM-SHA384', + 0xc02d: 'ECDH-ECDSA-AES128-GCM-SHA256', + 0xc02e: 'ECDH-ECDSA-AES256-GCM-SHA384', + 0xc02f: 'ECDHE-RSA-AES128-GCM-SHA256', + 0xc030: 'ECDHE-RSA-AES256-GCM-SHA384', + 0xc031: 'ECDH-RSA-AES128-GCM-SHA256', + 0xc032: 'ECDH-RSA-AES256-GCM-SHA384', + 0xcc13: 'ECDHE-RSA-CHACHA20-POLY1305', + 0xcc14: 'ECDHE-ECDSA-CHACHA20-POLY1305', + 0xcc15: 'DHE-RSA-CHACHA20-POLY1305', + 0xff00: 'GOST-MD5', + 0xff01: 'GOST-GOST94', + 0xff02: 'GOST-GOST89MAC', + 0xff03: 'GOST-GOST89STREAM', + 0x010080: 'RC4-MD5', + 0x020080: 'EXP-RC4-MD5', + 0x030080: 'RC2-CBC-MD5', + 0x040080: 'EXP-RC2-CBC-MD5', + 0x050080: 'IDEA-CBC-MD5', + 0x060040: 'DES-CBC-MD5', + 0x0700c0: 'DES-CBC3-MD5', + 0x080080: 'RC4-64-MD5', +} + + +def is_tls_record_magic(d): + """ + Returns: + True, if the passed bytes start with the TLS record magic bytes. + False, otherwise. + """ + d = d[:3] + + # TLS ClientHello magic, works for SSLv3, TLSv1.0, TLSv1.1, TLSv1.2 + # http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html#client-hello + return ( + len(d) == 3 and + d[0] == '\x16' and + d[1] == '\x03' and + d[2] in ('\x00', '\x01', '\x02', '\x03') + ) + + +def get_client_hello(client_conn): + """ + Peek into the socket and read all records that contain the initial client hello message. + + client_conn: + The :py:class:`client connection `. + + Returns: + The raw handshake packet bytes, without TLS record header(s). + """ + client_hello = "" + client_hello_size = 1 + offset = 0 + while len(client_hello) < client_hello_size: + record_header = client_conn.rfile.peek(offset + 5)[offset:] + if not is_tls_record_magic(record_header) or len(record_header) != 5: + raise TlsProtocolException('Expected TLS record, got "%s" instead.' % record_header) + record_size = struct.unpack("!H", record_header[3:])[0] + 5 + record_body = client_conn.rfile.peek(offset + record_size)[offset + 5:] + if len(record_body) != record_size - 5: + raise TlsProtocolException("Unexpected EOF in TLS handshake: %s" % record_body) + client_hello += record_body + offset += record_size + client_hello_size = struct.unpack("!I", '\x00' + client_hello[1:4])[0] + 4 + return client_hello + + +class TlsClientHello(object): + + def __init__(self, raw_client_hello): + self._client_hello = ClientHello.parse(raw_client_hello) + + def raw(self): + return self._client_hello + + @property + def client_cipher_suites(self): + return self._client_hello.cipher_suites.cipher_suites + + @property + def client_sni(self): + for extension in self._client_hello.extensions: + if (extension.type == 0x00 and len(extension.server_names) == 1 + and extension.server_names[0].type == 0): + return extension.server_names[0].name + + @property + def client_alpn_protocols(self): + for extension in self._client_hello.extensions: + if extension.type == 0x10: + return list(extension.alpn_protocols) + + @classmethod + def from_client_conn(cls, client_conn): + """ + Peek into the connection, read the initial client hello and parse it to obtain ALPN values. + client_conn: + The :py:class:`client connection `. + Returns: + :py:class:`client hello `. + """ + try: + raw_client_hello = get_client_hello(client_conn)[4:] # exclude handshake header. + except ProtocolException as e: + raise TlsProtocolException('Cannot read raw Client Hello: %s' % repr(e)) + + try: + return cls(raw_client_hello) + except ConstructError as e: + raise TlsProtocolException('Cannot parse Client Hello: %s, Raw Client Hello: %s' % + (repr(e), raw_client_hello.encode("hex"))) + + def __repr__(self): + return "TlsClientHello( sni: %s alpn_protocols: %s, cipher_suites: %s)" % \ + (self.client_sni, self.client_alpn_protocols, self.client_cipher_suites) + + +class TlsLayer(Layer): + + def __init__(self, ctx, client_tls, server_tls): + self.client_sni = None + self.client_alpn_protocols = None + self.client_ciphers = [] + + super(TlsLayer, self).__init__(ctx) + self._client_tls = client_tls + self._server_tls = server_tls + + self._sni_from_server_change = None + + def __call__(self): + """ + The strategy for establishing SSL is as follows: + First, we determine whether we need the server cert to establish ssl with the client. + If so, we first connect to the server and then to the client. + If not, we only connect to the client and do the server_ssl lazily on a Connect message. + + An additional complexity is that establish ssl with the server may require a SNI value from + the client. In an ideal world, we'd do the following: + 1. Start the SSL handshake with the client + 2. Check if the client sends a SNI. + 3. Pause the client handshake, establish SSL with the server. + 4. Finish the client handshake with the certificate from the server. + There's just one issue: We cannot get a callback from OpenSSL if the client doesn't send a SNI. :( + Thus, we manually peek into the connection and parse the ClientHello message to obtain both SNI and ALPN values. + + Further notes: + - OpenSSL 1.0.2 introduces a callback that would help here: + https://www.openssl.org/docs/ssl/SSL_CTX_set_cert_cb.html + - The original mitmproxy issue is https://github.com/mitmproxy/mitmproxy/issues/427 + """ + + client_tls_requires_server_cert = ( + self._client_tls and self._server_tls and not self.config.no_upstream_cert + ) + + if self._client_tls: + self._parse_client_hello() + + if client_tls_requires_server_cert: + self._establish_tls_with_client_and_server() + elif self._client_tls: + self._establish_tls_with_client() + + layer = self.ctx.next_layer(self) + layer() + + def __repr__(self): # pragma: no cover + if self._client_tls and self._server_tls: + return "TlsLayer(client and server)" + elif self._client_tls: + return "TlsLayer(client)" + elif self._server_tls: + return "TlsLayer(server)" + else: + return "TlsLayer(inactive)" + + def _parse_client_hello(self): + """ + Peek into the connection, read the initial client hello and parse it to obtain ALPN values. + """ + try: + parsed = TlsClientHello.from_client_conn(self.client_conn) + self.client_sni = parsed.client_sni + self.client_alpn_protocols = parsed.client_alpn_protocols + self.client_ciphers = parsed.client_cipher_suites + except TlsProtocolException as e: + self.log("Cannot parse Client Hello: %s" % repr(e), "error") + + def connect(self): + if not self.server_conn: + self.ctx.connect() + if self._server_tls and not self.server_conn.tls_established: + self._establish_tls_with_server() + + def set_server(self, address, server_tls=None, sni=None): + if server_tls is not None: + self._sni_from_server_change = sni + self._server_tls = server_tls + self.ctx.set_server(address, None, None) + + @property + def sni_for_server_connection(self): + if self._sni_from_server_change is False: + return None + else: + return self._sni_from_server_change or self.client_sni + + @property + def alpn_for_client_connection(self): + return self.server_conn.get_alpn_proto_negotiated() + + def __alpn_select_callback(self, conn_, options): + """ + Once the client signals the alternate protocols it supports, + we reconnect upstream with the same list and pass the server's choice down to the client. + """ + + # This gets triggered if we haven't established an upstream connection yet. + default_alpn = b'http/1.1' + # alpn_preference = b'h2' + + if self.alpn_for_client_connection in options: + choice = bytes(self.alpn_for_client_connection) + elif default_alpn in options: + choice = bytes(default_alpn) + else: + choice = options[0] + self.log("ALPN for client: %s" % choice, "debug") + return choice + + def _establish_tls_with_client_and_server(self): + # If establishing TLS with the server fails, we try to establish TLS with the client nonetheless + # to send an error message over TLS. + try: + self.ctx.connect() + self._establish_tls_with_server() + except Exception: + try: + self._establish_tls_with_client() + except: + pass + six.reraise(*sys.exc_info()) + + self._establish_tls_with_client() + + def _establish_tls_with_client(self): + self.log("Establish TLS with client", "debug") + cert, key, chain_file = self._find_cert() + + try: + self.client_conn.convert_to_ssl( + cert, key, + method=self.config.openssl_method_client, + options=self.config.openssl_options_client, + cipher_list=self.config.ciphers_client, + dhparams=self.config.certstore.dhparams, + chain_file=chain_file, + alpn_select_callback=self.__alpn_select_callback, + ) + # Some TLS clients will not fail the handshake, + # but will immediately throw an "unexpected eof" error on the first read. + # The reason for this might be difficult to find, so we try to peek here to see if it + # raises ann error. + self.client_conn.rfile.peek(1) + except TlsException as e: + six.reraise( + ClientHandshakeException, + ClientHandshakeException( + "Cannot establish TLS with client (sni: {sni}): {e}".format( + sni=self.client_sni, e=repr(e) + ), + self.client_sni or repr(self.server_conn.address) + ), + sys.exc_info()[2] + ) + + def _establish_tls_with_server(self): + self.log("Establish TLS with server", "debug") + try: + # We only support http/1.1 and h2. + # If the server only supports spdy (next to http/1.1), it may select that + # and mitmproxy would enter TCP passthrough mode, which we want to avoid. + deprecated_http2_variant = lambda x: x.startswith("h2-") or x.startswith("spdy") + if self.client_alpn_protocols: + alpn = [x for x in self.client_alpn_protocols if not deprecated_http2_variant(x)] + else: + alpn = None + if alpn and "h2" in alpn and not self.config.http2: + alpn.remove("h2") + + ciphers_server = self.config.ciphers_server + if not ciphers_server: + ciphers_server = [] + for id in self.client_ciphers: + if id in CIPHER_ID_NAME_MAP.keys(): + ciphers_server.append(CIPHER_ID_NAME_MAP[id]) + ciphers_server = ':'.join(ciphers_server) + + self.server_conn.establish_ssl( + self.config.clientcerts, + self.sni_for_server_connection, + method=self.config.openssl_method_server, + options=self.config.openssl_options_server, + verify_options=self.config.openssl_verification_mode_server, + ca_path=self.config.openssl_trusted_cadir_server, + ca_pemfile=self.config.openssl_trusted_ca_server, + cipher_list=ciphers_server, + alpn_protos=alpn, + ) + tls_cert_err = self.server_conn.ssl_verification_error + if tls_cert_err is not None: + self.log( + "TLS verification failed for upstream server at depth %s with error: %s" % + (tls_cert_err['depth'], tls_cert_err['errno']), + "error") + self.log("Ignoring server verification error, continuing with connection", "error") + except InvalidCertificateException as e: + tls_cert_err = self.server_conn.ssl_verification_error + self.log( + "TLS verification failed for upstream server at depth %s with error: %s" % + (tls_cert_err['depth'], tls_cert_err['errno']), + "error") + self.log("Aborting connection attempt", "error") + six.reraise( + TlsProtocolException, + TlsProtocolException("Cannot establish TLS with {address} (sni: {sni}): {e}".format( + address=repr(self.server_conn.address), + sni=self.sni_for_server_connection, + e=repr(e), + )), + sys.exc_info()[2] + ) + except TlsException as e: + six.reraise( + TlsProtocolException, + TlsProtocolException("Cannot establish TLS with {address} (sni: {sni}): {e}".format( + address=repr(self.server_conn.address), + sni=self.sni_for_server_connection, + e=repr(e), + )), + sys.exc_info()[2] + ) + + self.log("ALPN selected by server: %s" % self.alpn_for_client_connection, "debug") + + def _find_cert(self): + """ + This function determines the Common Name (CN) and Subject Alternative Names (SANs) + our certificate should have and then fetches a matching cert from the certstore. + """ + host = None + sans = set() + + # In normal operation, the server address should always be known at this point. + # However, we may just want to establish TLS so that we can send an error message to the client, + # in which case the address can be None. + if self.server_conn.address: + host = self.server_conn.address.host + + # Should we incorporate information from the server certificate? + use_upstream_cert = ( + self.server_conn and + self.server_conn.tls_established and + (not self.config.no_upstream_cert) + ) + if use_upstream_cert: + upstream_cert = self.server_conn.cert + sans.update(upstream_cert.altnames) + if upstream_cert.cn: + sans.add(host) + host = upstream_cert.cn.decode("utf8").encode("idna") + # Also add SNI values. + if self.client_sni: + sans.add(self.client_sni) + if self._sni_from_server_change: + sans.add(self._sni_from_server_change) + + # Some applications don't consider the CN and expect the hostname to be in the SANs. + # For example, Thunderbird 38 will display a warning if the remote host is only the CN. + sans.add(host) + return self.config.certstore.get_cert(host, list(sans)) diff --git a/mitmproxy/proxy/__init__.py b/mitmproxy/proxy/__init__.py new file mode 100644 index 00000000..be7f5207 --- /dev/null +++ b/mitmproxy/proxy/__init__.py @@ -0,0 +1,11 @@ +from __future__ import (absolute_import, print_function, division) + +from .server import ProxyServer, DummyServer +from .config import ProxyConfig +from .root_context import RootContext, Log + +__all__ = [ + "ProxyServer", "DummyServer", + "ProxyConfig", + "RootContext", "Log", +] diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py new file mode 100644 index 00000000..a635ab19 --- /dev/null +++ b/mitmproxy/proxy/config.py @@ -0,0 +1,208 @@ +from __future__ import (absolute_import, print_function, division) +import collections +import os +import re +from OpenSSL import SSL + +from netlib import certutils, tcp +from netlib.http import authentication +from netlib.tcp import Address, sslversion_choices + +from .. import utils, platform + +CONF_BASENAME = "mitmproxy" +CA_DIR = "~/.mitmproxy" + +# We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default. +# https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=apache-2.2.15&openssl=1.0.2&hsts=yes&profile=old +DEFAULT_CLIENT_CIPHERS = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA" + + +class HostMatcher(object): + + def __init__(self, patterns=tuple()): + self.patterns = list(patterns) + self.regexes = [re.compile(p, re.IGNORECASE) for p in self.patterns] + + def __call__(self, address): + if not address: + return False + address = tcp.Address.wrap(address) + host = "%s:%s" % (address.host, address.port) + if any(rex.search(host) for rex in self.regexes): + return True + else: + return False + + def __nonzero__(self): + return bool(self.patterns) + + +ServerSpec = collections.namedtuple("ServerSpec", "scheme address") + + +class ProxyConfig: + + def __init__( + self, + host='', + port=8080, + cadir=CA_DIR, + clientcerts=None, + no_upstream_cert=False, + body_size_limit=None, + mode="regular", + upstream_server=None, + authenticator=None, + ignore_hosts=tuple(), + tcp_hosts=tuple(), + http2=False, + rawtcp=False, + ciphers_client=DEFAULT_CLIENT_CIPHERS, + ciphers_server=None, + certs=tuple(), + ssl_version_client="secure", + ssl_version_server="secure", + ssl_verify_upstream_cert=False, + ssl_verify_upstream_trusted_cadir=None, + ssl_verify_upstream_trusted_ca=None, + ): + self.host = host + self.port = port + self.ciphers_client = ciphers_client + self.ciphers_server = ciphers_server + self.clientcerts = clientcerts + self.no_upstream_cert = no_upstream_cert + self.body_size_limit = body_size_limit + self.mode = mode + if upstream_server: + self.upstream_server = ServerSpec(upstream_server[0], Address.wrap(upstream_server[1])) + else: + self.upstream_server = None + + self.check_ignore = HostMatcher(ignore_hosts) + self.check_tcp = HostMatcher(tcp_hosts) + self.http2 = http2 + self.rawtcp = rawtcp + self.authenticator = authenticator + self.cadir = os.path.expanduser(cadir) + self.certstore = certutils.CertStore.from_store( + self.cadir, + CONF_BASENAME + ) + for spec, cert in certs: + self.certstore.add_cert_file(spec, cert) + + self.openssl_method_client, self.openssl_options_client = \ + sslversion_choices[ssl_version_client] + self.openssl_method_server, self.openssl_options_server = \ + sslversion_choices[ssl_version_server] + + if ssl_verify_upstream_cert: + self.openssl_verification_mode_server = SSL.VERIFY_PEER + else: + self.openssl_verification_mode_server = SSL.VERIFY_NONE + self.openssl_trusted_cadir_server = ssl_verify_upstream_trusted_cadir + self.openssl_trusted_ca_server = ssl_verify_upstream_trusted_ca + + +def process_proxy_options(parser, options): + body_size_limit = utils.parse_size(options.body_size_limit) + + c = 0 + mode, upstream_server = "regular", None + if options.transparent_proxy: + c += 1 + if not platform.resolver: + return parser.error("Transparent mode not supported on this platform.") + mode = "transparent" + if options.socks_proxy: + c += 1 + mode = "socks5" + if options.reverse_proxy: + c += 1 + mode = "reverse" + upstream_server = options.reverse_proxy + if options.upstream_proxy: + c += 1 + mode = "upstream" + upstream_server = options.upstream_proxy + if c > 1: + return parser.error( + "Transparent, SOCKS5, reverse and upstream proxy mode " + "are mutually exclusive. Read the docs on proxy modes to understand why." + ) + + if options.clientcerts: + options.clientcerts = os.path.expanduser(options.clientcerts) + if not os.path.exists(options.clientcerts): + return parser.error( + "Client certificate path does not exist: %s" % options.clientcerts + ) + + if options.auth_nonanonymous or options.auth_singleuser or options.auth_htpasswd: + + if options.transparent_proxy: + return parser.error("Proxy Authentication not supported in transparent mode.") + + if options.socks_proxy: + return parser.error( + "Proxy Authentication not supported in SOCKS mode. " + "https://github.com/mitmproxy/mitmproxy/issues/738" + ) + + if options.auth_singleuser: + if len(options.auth_singleuser.split(':')) != 2: + return parser.error( + "Invalid single-user specification. Please use the format username:password" + ) + username, password = options.auth_singleuser.split(':') + password_manager = authentication.PassManSingleUser(username, password) + elif options.auth_nonanonymous: + password_manager = authentication.PassManNonAnon() + elif options.auth_htpasswd: + try: + password_manager = authentication.PassManHtpasswd( + options.auth_htpasswd) + except ValueError as v: + return parser.error(v.message) + authenticator = authentication.BasicProxyAuth(password_manager, "mitmproxy") + else: + authenticator = authentication.NullProxyAuth(None) + + certs = [] + for i in options.certs: + parts = i.split("=", 1) + if len(parts) == 1: + parts = ["*", parts[0]] + parts[1] = os.path.expanduser(parts[1]) + if not os.path.exists(parts[1]): + parser.error("Certificate file does not exist: %s" % parts[1]) + certs.append(parts) + + if options.http2 and not tcp.HAS_ALPN: + raise RuntimeError("HTTP2 support requires OpenSSL 1.0.2 or above.") + + return ProxyConfig( + host=options.addr, + port=options.port, + cadir=options.cadir, + clientcerts=options.clientcerts, + no_upstream_cert=options.no_upstream_cert, + body_size_limit=body_size_limit, + mode=mode, + upstream_server=upstream_server, + ignore_hosts=options.ignore_hosts, + tcp_hosts=options.tcp_hosts, + http2=options.http2, + rawtcp=options.rawtcp, + authenticator=authenticator, + ciphers_client=options.ciphers_client, + ciphers_server=options.ciphers_server, + certs=tuple(certs), + ssl_version_client=options.ssl_version_client, + ssl_version_server=options.ssl_version_server, + ssl_verify_upstream_cert=options.ssl_verify_upstream_cert, + ssl_verify_upstream_trusted_cadir=options.ssl_verify_upstream_trusted_cadir, + ssl_verify_upstream_trusted_ca=options.ssl_verify_upstream_trusted_ca + ) diff --git a/mitmproxy/proxy/modes/__init__.py b/mitmproxy/proxy/modes/__init__.py new file mode 100644 index 00000000..f014ed98 --- /dev/null +++ b/mitmproxy/proxy/modes/__init__.py @@ -0,0 +1,12 @@ +from __future__ import (absolute_import, print_function, division) +from .http_proxy import HttpProxy, HttpUpstreamProxy +from .reverse_proxy import ReverseProxy +from .socks_proxy import Socks5Proxy +from .transparent_proxy import TransparentProxy + +__all__ = [ + "HttpProxy", "HttpUpstreamProxy", + "ReverseProxy", + "Socks5Proxy", + "TransparentProxy" +] diff --git a/mitmproxy/proxy/modes/http_proxy.py b/mitmproxy/proxy/modes/http_proxy.py new file mode 100644 index 00000000..e19062b9 --- /dev/null +++ b/mitmproxy/proxy/modes/http_proxy.py @@ -0,0 +1,28 @@ +from __future__ import (absolute_import, print_function, division) + +from ...protocol import Layer, ServerConnectionMixin + + +class HttpProxy(Layer, ServerConnectionMixin): + + def __call__(self): + layer = self.ctx.next_layer(self) + try: + layer() + finally: + if self.server_conn: + self.disconnect() + + +class HttpUpstreamProxy(Layer, ServerConnectionMixin): + + def __init__(self, ctx, server_address): + super(HttpUpstreamProxy, self).__init__(ctx, server_address=server_address) + + def __call__(self): + layer = self.ctx.next_layer(self) + try: + layer() + finally: + if self.server_conn: + self.disconnect() diff --git a/mitmproxy/proxy/modes/reverse_proxy.py b/mitmproxy/proxy/modes/reverse_proxy.py new file mode 100644 index 00000000..c8e80a10 --- /dev/null +++ b/mitmproxy/proxy/modes/reverse_proxy.py @@ -0,0 +1,18 @@ +from __future__ import (absolute_import, print_function, division) + +from ...protocol import Layer, ServerConnectionMixin + + +class ReverseProxy(Layer, ServerConnectionMixin): + + def __init__(self, ctx, server_address, server_tls): + super(ReverseProxy, self).__init__(ctx, server_address=server_address) + self.server_tls = server_tls + + def __call__(self): + layer = self.ctx.next_layer(self) + try: + layer() + finally: + if self.server_conn: + self.disconnect() diff --git a/mitmproxy/proxy/modes/socks_proxy.py b/mitmproxy/proxy/modes/socks_proxy.py new file mode 100644 index 00000000..e2ce44ae --- /dev/null +++ b/mitmproxy/proxy/modes/socks_proxy.py @@ -0,0 +1,66 @@ +from __future__ import (absolute_import, print_function, division) + +from netlib import socks, tcp +from netlib.exceptions import TcpException + +from ...exceptions import Socks5ProtocolException +from ...protocol import Layer, ServerConnectionMixin + + +class Socks5Proxy(Layer, ServerConnectionMixin): + + def __init__(self, ctx): + super(Socks5Proxy, self).__init__(ctx) + + def __call__(self): + try: + # Parse Client Greeting + client_greet = socks.ClientGreeting.from_file(self.client_conn.rfile, fail_early=True) + client_greet.assert_socks5() + if socks.METHOD.NO_AUTHENTICATION_REQUIRED not in client_greet.methods: + raise socks.SocksError( + socks.METHOD.NO_ACCEPTABLE_METHODS, + "mitmproxy only supports SOCKS without authentication" + ) + + # Send Server Greeting + server_greet = socks.ServerGreeting( + socks.VERSION.SOCKS5, + socks.METHOD.NO_AUTHENTICATION_REQUIRED + ) + server_greet.to_file(self.client_conn.wfile) + self.client_conn.wfile.flush() + + # Parse Connect Request + connect_request = socks.Message.from_file(self.client_conn.rfile) + connect_request.assert_socks5() + if connect_request.msg != socks.CMD.CONNECT: + raise socks.SocksError( + socks.REP.COMMAND_NOT_SUPPORTED, + "mitmproxy only supports SOCKS5 CONNECT." + ) + + # We always connect lazily, but we need to pretend to the client that we connected. + connect_reply = socks.Message( + socks.VERSION.SOCKS5, + socks.REP.SUCCEEDED, + connect_request.atyp, + # dummy value, we don't have an upstream connection yet. + connect_request.addr + ) + connect_reply.to_file(self.client_conn.wfile) + self.client_conn.wfile.flush() + + except (socks.SocksError, TcpException) as e: + raise Socks5ProtocolException("SOCKS5 mode failure: %s" % repr(e)) + + # https://github.com/mitmproxy/mitmproxy/issues/839 + address_bytes = (connect_request.addr.host.encode("idna"), connect_request.addr.port) + self.server_conn.address = tcp.Address(address_bytes, connect_request.addr.use_ipv6) + + layer = self.ctx.next_layer(self) + try: + layer() + finally: + if self.server_conn: + self.disconnect() diff --git a/mitmproxy/proxy/modes/transparent_proxy.py b/mitmproxy/proxy/modes/transparent_proxy.py new file mode 100644 index 00000000..3fdda656 --- /dev/null +++ b/mitmproxy/proxy/modes/transparent_proxy.py @@ -0,0 +1,25 @@ +from __future__ import (absolute_import, print_function, division) + +from ... import platform +from ...exceptions import ProtocolException +from ...protocol import Layer, ServerConnectionMixin + + +class TransparentProxy(Layer, ServerConnectionMixin): + + def __init__(self, ctx): + super(TransparentProxy, self).__init__(ctx) + self.resolver = platform.resolver() + + def __call__(self): + try: + self.server_conn.address = self.resolver.original_addr(self.client_conn.connection) + except Exception as e: + raise ProtocolException("Transparent mode failure: %s" % repr(e)) + + layer = self.ctx.next_layer(self) + try: + layer() + finally: + if self.server_conn: + self.disconnect() diff --git a/mitmproxy/proxy/root_context.py b/mitmproxy/proxy/root_context.py new file mode 100644 index 00000000..ae49523f --- /dev/null +++ b/mitmproxy/proxy/root_context.py @@ -0,0 +1,139 @@ +from __future__ import (absolute_import, print_function, division) +import string +import sys + +import six + +from mitmproxy.exceptions import ProtocolException, TlsProtocolException +from netlib.exceptions import TcpException +from ..protocol import ( + RawTCPLayer, TlsLayer, Http1Layer, Http2Layer, is_tls_record_magic, ServerConnectionMixin, + UpstreamConnectLayer, TlsClientHello +) +from .modes import HttpProxy, HttpUpstreamProxy, ReverseProxy + + +class RootContext(object): + + """ + The outermost context provided to the root layer. + As a consequence, every layer has access to methods and attributes defined here. + + Attributes: + client_conn: + The :py:class:`client connection `. + channel: + A :py:class:`~mitmproxy.controller.Channel` to communicate with the FlowMaster. + Provides :py:meth:`.ask() ` and + :py:meth:`.tell() ` methods. + config: + The :py:class:`proxy server's configuration ` + """ + + def __init__(self, client_conn, config, channel): + self.client_conn = client_conn + self.channel = channel + self.config = config + + def next_layer(self, top_layer): + """ + This function determines the next layer in the protocol stack. + + Arguments: + top_layer: the current innermost layer. + + Returns: + The next layer + """ + layer = self._next_layer(top_layer) + return self.channel.ask("next_layer", layer) + + def _next_layer(self, top_layer): + try: + d = top_layer.client_conn.rfile.peek(3) + except TcpException as e: + six.reraise(ProtocolException, ProtocolException(str(e)), sys.exc_info()[2]) + client_tls = is_tls_record_magic(d) + + # 1. check for --ignore + if self.config.check_ignore: + ignore = self.config.check_ignore(top_layer.server_conn.address) + if not ignore and client_tls: + try: + client_hello = TlsClientHello.from_client_conn(self.client_conn) + except TlsProtocolException as e: + self.log("Cannot parse Client Hello: %s" % repr(e), "error") + else: + ignore = self.config.check_ignore((client_hello.client_sni, 443)) + if ignore: + return RawTCPLayer(top_layer, logging=False) + + # 2. Always insert a TLS layer, even if there's neither client nor server tls. + # An inline script may upgrade from http to https, + # in which case we need some form of TLS layer. + if isinstance(top_layer, ReverseProxy): + return TlsLayer(top_layer, client_tls, top_layer.server_tls) + if isinstance(top_layer, ServerConnectionMixin) or isinstance(top_layer, UpstreamConnectLayer): + return TlsLayer(top_layer, client_tls, client_tls) + + # 3. In Http Proxy mode and Upstream Proxy mode, the next layer is fixed. + if isinstance(top_layer, TlsLayer): + if isinstance(top_layer.ctx, HttpProxy): + return Http1Layer(top_layer, "regular") + if isinstance(top_layer.ctx, HttpUpstreamProxy): + return Http1Layer(top_layer, "upstream") + + # 4. Check for other TLS cases (e.g. after CONNECT). + if client_tls: + return TlsLayer(top_layer, True, True) + + # 4. Check for --tcp + if self.config.check_tcp(top_layer.server_conn.address): + return RawTCPLayer(top_layer) + + # 5. Check for TLS ALPN (HTTP1/HTTP2) + if isinstance(top_layer, TlsLayer): + alpn = top_layer.client_conn.get_alpn_proto_negotiated() + if alpn == b'h2': + return Http2Layer(top_layer, 'transparent') + if alpn == b'http/1.1': + return Http1Layer(top_layer, 'transparent') + + # 6. Check for raw tcp mode + is_ascii = ( + len(d) == 3 and + # better be safe here and don't expect uppercase... + all(x in string.ascii_letters for x in d) + ) + if self.config.rawtcp and not is_ascii: + return RawTCPLayer(top_layer) + + # 7. Assume HTTP1 by default + return Http1Layer(top_layer, 'transparent') + + def log(self, msg, level, subs=()): + """ + Send a log message to the master. + """ + + full_msg = [ + "{}: {}".format(repr(self.client_conn.address), msg) + ] + for i in subs: + full_msg.append(" -> " + i) + full_msg = "\n".join(full_msg) + self.channel.tell("log", Log(full_msg, level)) + + @property + def layers(self): + return [] + + def __repr__(self): + return "RootContext" + + +class Log(object): + + def __init__(self, msg, level="info"): + self.msg = msg + self.level = level diff --git a/mitmproxy/proxy/server.py b/mitmproxy/proxy/server.py new file mode 100644 index 00000000..8b62ff93 --- /dev/null +++ b/mitmproxy/proxy/server.py @@ -0,0 +1,157 @@ +from __future__ import (absolute_import, print_function, division) + +import traceback +import sys +import socket +import six + +from netlib import tcp +from netlib.exceptions import TcpException +from netlib.http.http1 import assemble_response +from ..exceptions import ProtocolException, ServerException, ClientHandshakeException +from ..protocol import Kill +from ..models import ClientConnection, make_error_response +from .modes import HttpUpstreamProxy, HttpProxy, ReverseProxy, TransparentProxy, Socks5Proxy +from .root_context import RootContext, Log + + +class DummyServer: + bound = False + + def __init__(self, config): + self.config = config + + def start_slave(self, *args): + pass + + def shutdown(self): + pass + + +class ProxyServer(tcp.TCPServer): + allow_reuse_address = True + bound = True + + def __init__(self, config): + """ + Raises ProxyServerError if there's a startup problem. + """ + self.config = config + try: + super(ProxyServer, self).__init__((config.host, config.port)) + except socket.error as e: + six.reraise( + ServerException, + ServerException('Error starting proxy server: ' + repr(e)), + sys.exc_info()[2] + ) + self.channel = None + + def start_slave(self, klass, channel): + slave = klass(channel, self) + slave.start() + + def set_channel(self, channel): + self.channel = channel + + def handle_client_connection(self, conn, client_address): + h = ConnectionHandler( + conn, + client_address, + self.config, + self.channel + ) + h.handle() + + +class ConnectionHandler(object): + + def __init__(self, client_conn, client_address, config, channel): + self.config = config + """@type: mitmproxy.proxy.config.ProxyConfig""" + self.client_conn = ClientConnection( + client_conn, + client_address, + None) + """@type: mitmproxy.proxy.connection.ClientConnection""" + self.channel = channel + """@type: mitmproxy.controller.Channel""" + + def _create_root_layer(self): + root_context = RootContext( + self.client_conn, + self.config, + self.channel + ) + + mode = self.config.mode + if mode == "upstream": + return HttpUpstreamProxy( + root_context, + self.config.upstream_server.address + ) + elif mode == "transparent": + return TransparentProxy(root_context) + elif mode == "reverse": + server_tls = self.config.upstream_server.scheme == "https" + return ReverseProxy( + root_context, + self.config.upstream_server.address, + server_tls + ) + elif mode == "socks5": + return Socks5Proxy(root_context) + elif mode == "regular": + return HttpProxy(root_context) + elif callable(mode): # pragma: no cover + return mode(root_context) + else: # pragma: no cover + raise ValueError("Unknown proxy mode: %s" % mode) + + def handle(self): + self.log("clientconnect", "info") + + root_layer = self._create_root_layer() + root_layer = self.channel.ask("clientconnect", root_layer) + if root_layer == Kill: + def root_layer(): + raise Kill() + + try: + root_layer() + except Kill: + self.log("Connection killed", "info") + except ProtocolException as e: + + if isinstance(e, ClientHandshakeException): + self.log( + "Client Handshake failed. " + "The client may not trust the proxy's certificate for {}.".format(e.server), + "error" + ) + self.log(repr(e), "debug") + else: + self.log(repr(e), "info") + + self.log(traceback.format_exc(), "debug") + # If an error propagates to the topmost level, + # we send an HTTP error response, which is both + # understandable by HTTP clients and humans. + try: + error_response = make_error_response(502, repr(e)) + self.client_conn.send(assemble_response(error_response)) + except TcpException: + pass + except Exception: + self.log(traceback.format_exc(), "error") + print(traceback.format_exc(), file=sys.stderr) + print("mitmproxy has crashed!", file=sys.stderr) + print("Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy", file=sys.stderr) + + self.log("clientdisconnect", "info") + self.channel.tell("clientdisconnect", root_layer) + self.client_conn.finish() + + def log(self, msg, level): + msg = "{}: {}".format(repr(self.client_conn.address), msg) + self.channel.tell("log", Log(msg, level)) diff --git a/mitmproxy/script/__init__.py b/mitmproxy/script/__init__.py new file mode 100644 index 00000000..3ee19b04 --- /dev/null +++ b/mitmproxy/script/__init__.py @@ -0,0 +1,13 @@ +from .script import Script +from .script_context import ScriptContext +from .concurrent import concurrent +from ..exceptions import ScriptException +from . import reloader + +__all__ = [ + "Script", + "ScriptContext", + "concurrent", + "ScriptException", + "reloader" +] diff --git a/mitmproxy/script/concurrent.py b/mitmproxy/script/concurrent.py new file mode 100644 index 00000000..f0f5e3cd --- /dev/null +++ b/mitmproxy/script/concurrent.py @@ -0,0 +1,63 @@ +""" +This module provides a @concurrent decorator primitive to +offload computations from mitmproxy's main master thread. +""" +from __future__ import absolute_import, print_function, division +import threading + + +class ReplyProxy(object): + + def __init__(self, original_reply, script_thread): + self.original_reply = original_reply + self.script_thread = script_thread + self._ignore_call = True + self.lock = threading.Lock() + + def __call__(self, *args, **kwargs): + with self.lock: + if self._ignore_call: + self.script_thread.start() + self._ignore_call = False + return + self.original_reply(*args, **kwargs) + + def __getattr__(self, k): + return getattr(self.original_reply, k) + + +def _handle_concurrent_reply(fn, o, *args, **kwargs): + # Make first call to o.reply a no op and start the script thread. + # We must not start the script thread before, as this may lead to a nasty race condition + # where the script thread replies a different response before the normal reply, which then gets swallowed. + + def run(): + fn(*args, **kwargs) + # If the script did not call .reply(), we have to do it now. + reply_proxy() + + script_thread = ScriptThread(target=run) + + reply_proxy = ReplyProxy(o.reply, script_thread) + o.reply = reply_proxy + + +class ScriptThread(threading.Thread): + name = "ScriptThread" + + +def concurrent(fn): + if fn.func_name in ( + "request", + "response", + "error", + "clientconnect", + "serverconnect", + "clientdisconnect", + "next_layer"): + def _concurrent(ctx, obj): + _handle_concurrent_reply(fn, obj, ctx, obj) + + return _concurrent + raise NotImplementedError( + "Concurrent decorator not supported for '%s' method." % fn.func_name) diff --git a/mitmproxy/script/reloader.py b/mitmproxy/script/reloader.py new file mode 100644 index 00000000..b4acf51b --- /dev/null +++ b/mitmproxy/script/reloader.py @@ -0,0 +1,46 @@ +import os +import sys +from watchdog.events import RegexMatchingEventHandler +if sys.platform == 'darwin': + from watchdog.observers.polling import PollingObserver as Observer +else: + from watchdog.observers import Observer +# The OSX reloader in watchdog 0.8.3 breaks when unobserving paths. +# We use the PollingObserver instead. + +_observers = {} + + +def watch(script, callback): + if script in _observers: + raise RuntimeError("Script already observed") + script_dir = os.path.dirname(os.path.abspath(script.args[0])) + script_name = os.path.basename(script.args[0]) + event_handler = _ScriptModificationHandler(callback, filename=script_name) + observer = Observer() + observer.schedule(event_handler, script_dir) + observer.start() + _observers[script] = observer + + +def unwatch(script): + observer = _observers.pop(script, None) + if observer: + observer.stop() + observer.join() + + +class _ScriptModificationHandler(RegexMatchingEventHandler): + + def __init__(self, callback, filename='.*'): + + super(_ScriptModificationHandler, self).__init__( + ignore_directories=True, + regexes=['.*' + filename] + ) + self.callback = callback + + def on_modified(self, event): + self.callback() + +__all__ = ["watch", "unwatch"] diff --git a/mitmproxy/script/script.py b/mitmproxy/script/script.py new file mode 100644 index 00000000..55778851 --- /dev/null +++ b/mitmproxy/script/script.py @@ -0,0 +1,100 @@ +""" +The script object representing mitmproxy inline scripts. +Script objects know nothing about mitmproxy or mitmproxy's API - this knowledge is provided +by the mitmproxy-specific ScriptContext. +""" +# Do not import __future__ here, this would apply transitively to the inline scripts. +import os +import shlex +import traceback +import sys +from ..exceptions import ScriptException + + +class Script(object): + + """ + Script object representing an inline script. + """ + + def __init__(self, command, context): + self.command = command + self.args = self.parse_command(command) + self.ctx = context + self.ns = None + self.load() + + @property + def filename(self): + return self.args[0] + + @staticmethod + def parse_command(command): + if not command or not command.strip(): + raise ScriptException("Empty script command.") + if os.name == "nt": # Windows: escape all backslashes in the path. + backslashes = shlex.split(command, posix=False)[0].count("\\") + command = command.replace("\\", "\\\\", backslashes) + args = shlex.split(command) + args[0] = os.path.expanduser(args[0]) + if not os.path.exists(args[0]): + raise ScriptException( + ("Script file not found: %s.\r\n" + "If your script path contains spaces, " + "make sure to wrap it in additional quotes, e.g. -s \"'./foo bar/baz.py' --args\".") % + args[0]) + elif os.path.isdir(args[0]): + raise ScriptException("Not a file: %s" % args[0]) + return args + + def load(self): + """ + Loads an inline script. + + Returns: + The return value of self.run("start", ...) + + Raises: + ScriptException on failure + """ + if self.ns is not None: + self.unload() + script_dir = os.path.dirname(os.path.abspath(self.args[0])) + self.ns = {'__file__': os.path.abspath(self.args[0])} + sys.path.append(script_dir) + try: + execfile(self.args[0], self.ns, self.ns) + except Exception as e: + # Python 3: use exception chaining, https://www.python.org/dev/peps/pep-3134/ + raise ScriptException(traceback.format_exc(e)) + finally: + sys.path.pop() + return self.run("start", self.args) + + def unload(self): + try: + return self.run("done") + finally: + self.ns = None + + def run(self, name, *args, **kwargs): + """ + Runs an inline script hook. + + Returns: + The return value of the method. + None, if the script does not provide the method. + + Raises: + ScriptException if there was an exception. + """ + if self.ns is None: + raise ScriptException("Script not loaded.") + f = self.ns.get(name) + if f: + try: + return f(self.ctx, *args, **kwargs) + except Exception as e: + raise ScriptException(traceback.format_exc(e)) + else: + return None diff --git a/mitmproxy/script/script_context.py b/mitmproxy/script/script_context.py new file mode 100644 index 00000000..cd5d4b61 --- /dev/null +++ b/mitmproxy/script/script_context.py @@ -0,0 +1,60 @@ +""" +The mitmproxy script context provides an API to inline scripts. +""" +from __future__ import absolute_import, print_function, division +from .. import contentviews + + +class ScriptContext(object): + + """ + The script context should be used to interact with the global mitmproxy state from within a + script. + """ + + def __init__(self, master): + self._master = master + + def log(self, message, level="info"): + """ + Logs an event. + + By default, only events with level "error" get displayed. This can be controlled with the "-v" switch. + How log messages are handled depends on the front-end. mitmdump will print them to stdout, + mitmproxy sends output to the eventlog for display ("e" keyboard shortcut). + """ + self._master.add_event(message, level) + + def kill_flow(self, f): + """ + Kills a flow immediately. No further data will be sent to the client or the server. + """ + f.kill(self._master) + + def duplicate_flow(self, f): + """ + Returns a duplicate of the specified flow. The flow is also + injected into the current state, and is ready for editing, replay, + etc. + """ + self._master.pause_scripts = True + f = self._master.duplicate_flow(f) + self._master.pause_scripts = False + return f + + def replay_request(self, f): + """ + Replay the request on the current flow. The response will be added + to the flow object. + """ + return self._master.replay_request(f, block=True, run_scripthooks=False) + + @property + def app_registry(self): + return self._master.apps + + def add_contentview(self, view_obj): + contentviews.add(view_obj) + + def remove_contentview(self, view_obj): + contentviews.remove(view_obj) diff --git a/mitmproxy/setup.cfg b/mitmproxy/setup.cfg deleted file mode 100644 index 1151288f..00000000 --- a/mitmproxy/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -max-line-length = 120 -max-complexity = 20 - -[pep8] -max-line-length = 120 -exclude = */contrib/* -ignore = E251 - -[pytest] -testpaths = test diff --git a/mitmproxy/setup.py b/mitmproxy/setup.py deleted file mode 100644 index 5f8c6d42..00000000 --- a/mitmproxy/setup.py +++ /dev/null @@ -1,107 +0,0 @@ -from setuptools import setup, find_packages -from codecs import open -import os -import sys - -# Based on https://github.com/pypa/sampleproject/blob/master/setup.py -# and https://python-packaging-user-guide.readthedocs.org/ - -here = os.path.abspath(os.path.dirname(__file__)) - -sys.path.append(os.path.join(here, "..", "netlib")) -from mitmproxy import version - -with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -setup( - name="mitmproxy", - version=version.VERSION, - description="An interactive, SSL-capable, man-in-the-middle HTTP proxy for penetration testers and software developers.", - long_description=long_description, - url="http://mitmproxy.org", - author="Aldo Cortesi", - author_email="aldo@corte.si", - license="MIT", - classifiers=[ - "License :: OSI Approved :: MIT License", - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Environment :: Console :: Curses", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Security", - "Topic :: Internet", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: Proxy Servers", - "Topic :: Software Development :: Testing" - ], - packages=find_packages(), - include_package_data=True, - entry_points={ - 'console_scripts': [ - 'mitmproxy = mitmproxy.main:mitmproxy', - 'mitmdump = mitmproxy.main:mitmdump', - 'mitmweb = mitmproxy.main:mitmweb' - ] - }, - # https://packaging.python.org/en/latest/requirements/#install-requires - # It is not considered best practice to use install_requires to pin dependencies to specific versions. - install_requires=[ - "netlib=={}".format(version.VERSION), - "h2>=2.1.2, <3.0", - "tornado>=4.3, <4.4", - "configargparse>=0.10, <0.11", - "pyperclip>=1.5.22, <1.6", - "blinker>=1.4, <1.5", - "pyparsing>=2.1,<2.2", - "html2text==2016.1.8", - "construct>=2.5.2, <2.6", - "six>=1.10, <1.11", - "Pillow>=3.1, <3.2", - "watchdog>=0.8.3, <0.9", - "click>=6.2, <7.0", - "urwid>=1.3.1, <1.4", - ], - extras_require={ - ':sys_platform == "win32"': [ - "pydivert>=0.0.7, <0.1", - "lxml==3.4.4", # there are no Windows wheels for newer versions, so we pin this. - ], - ':sys_platform != "win32"': [ - "lxml>=3.5.0, <3.6", - ], - # Do not use a range operator here: https://bitbucket.org/pypa/setuptools/issues/380 - # Ubuntu Trusty and other still ship with setuptools < 17.1 - ':python_version == "2.7"': [ - "enum34>=1.0.4, <2", - ], - 'dev': [ - "mock>=1.3.0, <1.4", - "pytest>=2.8.7, <2.9", - "pytest-xdist>=1.14, <1.15", - "pytest-cov>=2.2.1, <2.3", - "pytest-timeout>=1.0.0, <1.1", - "coveralls>=1.1, <1.2", - "pathod=={}".format(version.VERSION), - "sphinx>=1.3.5, <1.4", - "sphinx-autobuild>=0.5.2, <0.7", - "sphinxcontrib-documentedlist>=0.3.0, <0.4" - ], - 'contentviews': [ - "pyamf>=0.8.0, <0.9", - "protobuf>=2.6.1, <2.7", - "cssutils>=1.0.1, <1.1" - ], - 'examples': [ - "pytz==2015.7.0", - "harparser>=0.2, <0.3", - "beautifulsoup4>=4.4.1, <4.5", - ] - } -) diff --git a/mitmproxy/stateobject.py b/mitmproxy/stateobject.py new file mode 100644 index 00000000..a4a1ffda --- /dev/null +++ b/mitmproxy/stateobject.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import +from netlib.utils import Serializable + + +class StateObject(Serializable): + + """ + An object with serializable state. + + State attributes can either be serializable types(str, tuple, bool, ...) + or StateObject instances themselves. + """ + + _stateobject_attributes = None + """ + An attribute-name -> class-or-type dict containing all attributes that + should be serialized. If the attribute is a class, it must implement the + Serializable protocol. + """ + + def get_state(self): + """ + Retrieve object state. + """ + state = {} + for attr, cls in self._stateobject_attributes.iteritems(): + val = getattr(self, attr) + if hasattr(val, "get_state"): + state[attr] = val.get_state() + else: + state[attr] = val + return state + + def set_state(self, state): + """ + Load object state from data returned by a get_state call. + """ + state = state.copy() + for attr, cls in self._stateobject_attributes.iteritems(): + if state.get(attr) is None: + setattr(self, attr, state.pop(attr)) + else: + curr = getattr(self, attr) + if hasattr(curr, "set_state"): + curr.set_state(state.pop(attr)) + elif hasattr(cls, "from_state"): + obj = cls.from_state(state.pop(attr)) + setattr(self, attr, obj) + else: # primitive types such as int, str, ... + setattr(self, attr, cls(state.pop(attr))) + if state: + raise RuntimeWarning("Unexpected State in __setstate__: {}".format(state)) diff --git a/mitmproxy/tnetstring.py b/mitmproxy/tnetstring.py new file mode 100644 index 00000000..c5c185c6 --- /dev/null +++ b/mitmproxy/tnetstring.py @@ -0,0 +1,399 @@ +# imported from the tnetstring project: https://github.com/rfk/tnetstring +# +# Copyright (c) 2011 Ryan Kelly +# +# 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 THE +# AUTHORS OR COPYRIGHT HOLDERS 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. +""" +tnetstring: data serialization using typed netstrings +====================================================== + + +This is a data serialization library. It's a lot like JSON but it uses a +new syntax called "typed netstrings" that Zed has proposed for use in the +Mongrel2 webserver. It's designed to be simpler and easier to implement +than JSON, with a happy consequence of also being faster in many cases. + +An ordinary netstring is a blob of data prefixed with its length and postfixed +with a sanity-checking comma. The string "hello world" encodes like this:: + + 11:hello world, + +Typed netstrings add other datatypes by replacing the comma with a type tag. +Here's the integer 12345 encoded as a tnetstring:: + + 5:12345# + +And here's the list [12345,True,0] which mixes integers and bools:: + + 19:5:12345#4:true!1:0#] + +Simple enough? This module gives you the following functions: + + :dump: dump an object as a tnetstring to a file + :dumps: dump an object as a tnetstring to a string + :load: load a tnetstring-encoded object from a file + :loads: load a tnetstring-encoded object from a string + :pop: pop a tnetstring-encoded object from the front of a string + +Note that since parsing a tnetstring requires reading all the data into memory +at once, there's no efficiency gain from using the file-based versions of these +functions. They're only here so you can use load() to read precisely one +item from a file or socket without consuming any extra data. + +By default tnetstrings work only with byte strings, not unicode. If you want +unicode strings then pass an optional encoding to the various functions, +like so:: + + >>> print(repr(tnetstring.loads("2:\\xce\\xb1,"))) + '\\xce\\xb1' + >>> + >>> print(repr(tnetstring.loads("2:\\xce\\xb1,","utf8"))) + u'\u03b1' + +""" + +__ver_major__ = 0 +__ver_minor__ = 2 +__ver_patch__ = 0 +__ver_sub__ = "" +__version__ = "%d.%d.%d%s" % ( + __ver_major__, __ver_minor__, __ver_patch__, __ver_sub__) + + +from collections import deque + + +def dumps(value, encoding=None): + """dumps(object,encoding=None) -> string + + This function dumps a python object as a tnetstring. + """ + # This uses a deque to collect output fragments in reverse order, + # then joins them together at the end. It's measurably faster + # than creating all the intermediate strings. + # If you're reading this to get a handle on the tnetstring format, + # consider the _gdumps() function instead; it's a standard top-down + # generator that's simpler to understand but much less efficient. + q = deque() + _rdumpq(q, 0, value, encoding) + return "".join(q) + + +def dump(value, file, encoding=None): + """dump(object,file,encoding=None) + + This function dumps a python object as a tnetstring and writes it to + the given file. + """ + file.write(dumps(value, encoding)) + file.flush() + + +def _rdumpq(q, size, value, encoding=None): + """Dump value as a tnetstring, to a deque instance, last chunks first. + + This function generates the tnetstring representation of the given value, + pushing chunks of the output onto the given deque instance. It pushes + the last chunk first, then recursively generates more chunks. + + When passed in the current size of the string in the queue, it will return + the new size of the string in the queue. + + Operating last-chunk-first makes it easy to calculate the size written + for recursive structures without having to build their representation as + a string. This is measurably faster than generating the intermediate + strings, especially on deeply nested structures. + """ + write = q.appendleft + if value is None: + write("0:~") + return size + 3 + if value is True: + write("4:true!") + return size + 7 + if value is False: + write("5:false!") + return size + 8 + if isinstance(value, (int, long)): + data = str(value) + ldata = len(data) + span = str(ldata) + write("#") + write(data) + write(":") + write(span) + return size + 2 + len(span) + ldata + if isinstance(value, (float,)): + # Use repr() for float rather than str(). + # It round-trips more accurately. + # Probably unnecessary in later python versions that + # use David Gay's ftoa routines. + data = repr(value) + ldata = len(data) + span = str(ldata) + write("^") + write(data) + write(":") + write(span) + return size + 2 + len(span) + ldata + if isinstance(value, str): + lvalue = len(value) + span = str(lvalue) + write(",") + write(value) + write(":") + write(span) + return size + 2 + len(span) + lvalue + if isinstance(value, (list, tuple,)): + write("]") + init_size = size = size + 1 + for item in reversed(value): + size = _rdumpq(q, size, item, encoding) + span = str(size - init_size) + write(":") + write(span) + return size + 1 + len(span) + if isinstance(value, dict): + write("}") + init_size = size = size + 1 + for (k, v) in value.iteritems(): + size = _rdumpq(q, size, v, encoding) + size = _rdumpq(q, size, k, encoding) + span = str(size - init_size) + write(":") + write(span) + return size + 1 + len(span) + if isinstance(value, unicode): + if encoding is None: + raise ValueError("must specify encoding to dump unicode strings") + value = value.encode(encoding) + lvalue = len(value) + span = str(lvalue) + write(",") + write(value) + write(":") + write(span) + return size + 2 + len(span) + lvalue + raise ValueError("unserializable object") + + +def _gdumps(value, encoding): + """Generate fragments of value dumped as a tnetstring. + + This is the naive dumping algorithm, implemented as a generator so that + it's easy to pass to "".join() without building a new list. + + This is mainly here for comparison purposes; the _rdumpq version is + measurably faster as it doesn't have to build intermediate strins. + """ + if value is None: + yield "0:~" + elif value is True: + yield "4:true!" + elif value is False: + yield "5:false!" + elif isinstance(value, (int, long)): + data = str(value) + yield str(len(data)) + yield ":" + yield data + yield "#" + elif isinstance(value, (float,)): + data = repr(value) + yield str(len(data)) + yield ":" + yield data + yield "^" + elif isinstance(value, (str,)): + yield str(len(value)) + yield ":" + yield value + yield "," + elif isinstance(value, (list, tuple,)): + sub = [] + for item in value: + sub.extend(_gdumps(item)) + sub = "".join(sub) + yield str(len(sub)) + yield ":" + yield sub + yield "]" + elif isinstance(value, (dict,)): + sub = [] + for (k, v) in value.iteritems(): + sub.extend(_gdumps(k)) + sub.extend(_gdumps(v)) + sub = "".join(sub) + yield str(len(sub)) + yield ":" + yield sub + yield "}" + elif isinstance(value, (unicode,)): + if encoding is None: + raise ValueError("must specify encoding to dump unicode strings") + value = value.encode(encoding) + yield str(len(value)) + yield ":" + yield value + yield "," + else: + raise ValueError("unserializable object") + + +def loads(string, encoding=None): + """loads(string,encoding=None) -> object + + This function parses a tnetstring into a python object. + """ + # No point duplicating effort here. In the C-extension version, + # loads() is measurably faster then pop() since it can avoid + # the overhead of building a second string. + return pop(string, encoding)[0] + + +def load(file, encoding=None): + """load(file,encoding=None) -> object + + This function reads a tnetstring from a file and parses it into a + python object. The file must support the read() method, and this + function promises not to read more data than necessary. + """ + # Read the length prefix one char at a time. + # Note that the netstring spec explicitly forbids padding zeros. + c = file.read(1) + if not c.isdigit(): + raise ValueError("not a tnetstring: missing or invalid length prefix") + datalen = ord(c) - ord("0") + c = file.read(1) + if datalen != 0: + while c.isdigit(): + datalen = (10 * datalen) + (ord(c) - ord("0")) + if datalen > 999999999: + errmsg = "not a tnetstring: absurdly large length prefix" + raise ValueError(errmsg) + c = file.read(1) + if c != ":": + raise ValueError("not a tnetstring: missing or invalid length prefix") + # Now we can read and parse the payload. + # This repeats the dispatch logic of pop() so we can avoid + # re-constructing the outermost tnetstring. + data = file.read(datalen) + if len(data) != datalen: + raise ValueError("not a tnetstring: length prefix too big") + type = file.read(1) + if type == ",": + if encoding is not None: + return data.decode(encoding) + return data + if type == "#": + try: + return int(data) + except ValueError: + raise ValueError("not a tnetstring: invalid integer literal") + if type == "^": + try: + return float(data) + except ValueError: + raise ValueError("not a tnetstring: invalid float literal") + if type == "!": + if data == "true": + return True + elif data == "false": + return False + else: + raise ValueError("not a tnetstring: invalid boolean literal") + if type == "~": + if data: + raise ValueError("not a tnetstring: invalid null literal") + return None + if type == "]": + l = [] + while data: + (item, data) = pop(data, encoding) + l.append(item) + return l + if type == "}": + d = {} + while data: + (key, data) = pop(data, encoding) + (val, data) = pop(data, encoding) + d[key] = val + return d + raise ValueError("unknown type tag") + + +def pop(string, encoding=None): + """pop(string,encoding=None) -> (object, remain) + + This function parses a tnetstring into a python object. + It returns a tuple giving the parsed object and a string + containing any unparsed data from the end of the string. + """ + # Parse out data length, type and remaining string. + try: + (dlen, rest) = string.split(":", 1) + dlen = int(dlen) + except ValueError: + raise ValueError("not a tnetstring: missing or invalid length prefix") + try: + (data, type, remain) = (rest[:dlen], rest[dlen], rest[dlen + 1:]) + except IndexError: + # This fires if len(rest) < dlen, meaning we don't need + # to further validate that data is the right length. + raise ValueError("not a tnetstring: invalid length prefix") + # Parse the data based on the type tag. + if type == ",": + if encoding is not None: + return (data.decode(encoding), remain) + return (data, remain) + if type == "#": + try: + return (int(data), remain) + except ValueError: + raise ValueError("not a tnetstring: invalid integer literal") + if type == "^": + try: + return (float(data), remain) + except ValueError: + raise ValueError("not a tnetstring: invalid float literal") + if type == "!": + if data == "true": + return (True, remain) + elif data == "false": + return (False, remain) + else: + raise ValueError("not a tnetstring: invalid boolean literal") + if type == "~": + if data: + raise ValueError("not a tnetstring: invalid null literal") + return (None, remain) + if type == "]": + l = [] + while data: + (item, data) = pop(data, encoding) + l.append(item) + return (l, remain) + if type == "}": + d = {} + while data: + (key, data) = pop(data, encoding) + (val, data) = pop(data, encoding) + d[key] = val + return (d, remain) + raise ValueError("unknown type tag") diff --git a/mitmproxy/utils.py b/mitmproxy/utils.py new file mode 100644 index 00000000..4bdd036e --- /dev/null +++ b/mitmproxy/utils.py @@ -0,0 +1,176 @@ +from __future__ import (absolute_import, print_function, division) +import os +import datetime +import re +import time +import json +import importlib +import inspect + +def timestamp(): + """ + Returns a serializable UTC timestamp. + """ + return time.time() + + +def format_timestamp(s): + s = time.localtime(s) + d = datetime.datetime.fromtimestamp(time.mktime(s)) + return d.strftime("%Y-%m-%d %H:%M:%S") + + +def format_timestamp_with_milli(s): + d = datetime.datetime.fromtimestamp(s) + return d.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + + +def isBin(s): + """ + Does this string have any non-ASCII characters? + """ + for i in s: + i = ord(i) + if i < 9 or 13 < i < 32 or 126 < i: + return True + return False + + +def isMostlyBin(s): + s = s[:100] + return sum(isBin(ch) for ch in s) / len(s) > 0.3 + + +def isXML(s): + for i in s: + if i in "\n \t": + continue + elif i == "<": + return True + else: + return False + + +def pretty_json(s): + try: + p = json.loads(s) + except ValueError: + return None + return json.dumps(p, sort_keys=True, indent=4) + + +def pretty_duration(secs): + formatters = [ + (100, "{:.0f}s"), + (10, "{:2.1f}s"), + (1, "{:1.2f}s"), + ] + + for limit, formatter in formatters: + if secs >= limit: + return formatter.format(secs) + # less than 1 sec + return "{:.0f}ms".format(secs * 1000) + + +class Data: + + def __init__(self, name): + m = importlib.import_module(name) + dirname = os.path.dirname(inspect.getsourcefile(m)) + self.dirname = os.path.abspath(dirname) + + def path(self, path): + """ + Returns a path to the package data housed at 'path' under this + module.Path can be a path to a file, or to a directory. + + This function will raise ValueError if the path does not exist. + """ + fullpath = os.path.join(self.dirname, path) + if not os.path.exists(fullpath): + raise ValueError("dataPath: %s does not exist." % fullpath) + return fullpath +pkg_data = Data(__name__) + + +class LRUCache: + + """ + A simple LRU cache for generated values. + """ + + def __init__(self, size=100): + self.size = size + self.cache = {} + self.cacheList = [] + + def get(self, gen, *args): + """ + gen: A (presumably expensive) generator function. The identity of + gen is NOT taken into account by the cache. + *args: A list of immutable arguments, used to establish identiy by + *the cache, and passed to gen to generate values. + """ + if args in self.cache: + self.cacheList.remove(args) + self.cacheList.insert(0, args) + return self.cache[args] + else: + ret = gen(*args) + self.cacheList.insert(0, args) + self.cache[args] = ret + if len(self.cacheList) > self.size: + d = self.cacheList.pop() + self.cache.pop(d) + return ret + + +def clean_hanging_newline(t): + """ + Many editors will silently add a newline to the final line of a + document (I'm looking at you, Vim). This function fixes this common + problem at the risk of removing a hanging newline in the rare cases + where the user actually intends it. + """ + if t and t[-1] == "\n": + return t[:-1] + return t + + +def parse_size(s): + """ + Parses a size specification. Valid specifications are: + + 123: bytes + 123k: kilobytes + 123m: megabytes + 123g: gigabytes + """ + if not s: + return None + mult = None + if s[-1].lower() == "k": + mult = 1024**1 + elif s[-1].lower() == "m": + mult = 1024**2 + elif s[-1].lower() == "g": + mult = 1024**3 + + if mult: + s = s[:-1] + else: + mult = 1 + try: + return int(s) * mult + except ValueError: + raise ValueError("Invalid size specification: %s" % s) + + +def safe_subn(pattern, repl, target, *args, **kwargs): + """ + There are Unicode conversion problems with re.subn. We try to smooth + that over by casting the pattern and replacement to strings. We really + need a better solution that is aware of the actual content ecoding. + """ + return re.subn(str(pattern), str(repl), target, *args, **kwargs) diff --git a/mitmproxy/version.py b/mitmproxy/version.py new file mode 100644 index 00000000..63f60a8d --- /dev/null +++ b/mitmproxy/version.py @@ -0,0 +1,3 @@ +from __future__ import (absolute_import, print_function, division) + +from netlib.version import * \ No newline at end of file diff --git a/mitmproxy/web/__init__.py b/mitmproxy/web/__init__.py new file mode 100644 index 00000000..50c49e8d --- /dev/null +++ b/mitmproxy/web/__init__.py @@ -0,0 +1,215 @@ +from __future__ import absolute_import, print_function +import collections +import tornado.ioloop +import tornado.httpserver + +from netlib.http import authentication + +from .. import controller, flow +from . import app + + +class Stop(Exception): + pass + + +class WebFlowView(flow.FlowView): + + def __init__(self, store): + super(WebFlowView, self).__init__(store, None) + + def _add(self, f): + super(WebFlowView, self)._add(f) + app.ClientConnection.broadcast( + type="flows", + cmd="add", + data=app._strip_content(f.get_state()) + ) + + def _update(self, f): + super(WebFlowView, self)._update(f) + app.ClientConnection.broadcast( + type="flows", + cmd="update", + data=app._strip_content(f.get_state()) + ) + + def _remove(self, f): + super(WebFlowView, self)._remove(f) + app.ClientConnection.broadcast( + type="flows", + cmd="remove", + data=f.id + ) + + def _recalculate(self, flows): + super(WebFlowView, self)._recalculate(flows) + app.ClientConnection.broadcast( + type="flows", + cmd="reset" + ) + + +class WebState(flow.State): + + def __init__(self): + super(WebState, self).__init__() + self.view._close() + self.view = WebFlowView(self.flows) + + self._last_event_id = 0 + self.events = collections.deque(maxlen=1000) + + def add_event(self, e, level): + self._last_event_id += 1 + entry = { + "id": self._last_event_id, + "message": e, + "level": level + } + self.events.append(entry) + app.ClientConnection.broadcast( + type="events", + cmd="add", + data=entry + ) + + def clear(self): + super(WebState, self).clear() + self.events.clear() + app.ClientConnection.broadcast( + type="events", + cmd="reset", + data=[] + ) + + +class Options(object): + attributes = [ + "app", + "app_domain", + "app_ip", + "anticache", + "anticomp", + "client_replay", + "eventlog", + "keepserving", + "kill", + "intercept", + "no_server", + "refresh_server_playback", + "rfile", + "scripts", + "showhost", + "replacements", + "rheaders", + "setheaders", + "server_replay", + "stickycookie", + "stickyauth", + "stream_large_bodies", + "verbosity", + "wfile", + "nopop", + + "wdebug", + "wport", + "wiface", + "wauthenticator", + "wsingleuser", + "whtpasswd", + ] + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + for i in self.attributes: + if not hasattr(self, i): + setattr(self, i, None) + + def process_web_options(self, parser): + if self.wsingleuser or self.whtpasswd: + if self.wsingleuser: + if len(self.wsingleuser.split(':')) != 2: + return parser.error( + "Invalid single-user specification. Please use the format username:password" + ) + username, password = self.wsingleuser.split(':') + self.wauthenticator = authentication.PassManSingleUser(username, password) + elif self.whtpasswd: + try: + self.wauthenticator = authentication.PassManHtpasswd(self.whtpasswd) + except ValueError as v: + return parser.error(v.message) + else: + self.wauthenticator = None + + +class WebMaster(flow.FlowMaster): + + def __init__(self, server, options): + self.options = options + super(WebMaster, self).__init__(server, WebState()) + self.app = app.Application(self, self.options.wdebug, self.options.wauthenticator) + if options.rfile: + try: + self.load_flows_file(options.rfile) + except flow.FlowReadError as v: + self.add_event( + "Could not read flow file: %s" % v, + "error" + ) + + if options.outfile: + err = self.start_stream_to_path( + options.outfile[0], + options.outfile[1] + ) + if err: + print >> sys.stderr, "Stream file error:", err + sys.exit(1) + + if self.options.app: + self.start_app(self.options.app_host, self.options.app_port) + + def tick(self): + flow.FlowMaster.tick(self, self.masterq, timeout=0) + + def run(self): # pragma: no cover + self.server.start_slave( + controller.Slave, + controller.Channel(self.masterq, self.should_exit) + ) + iol = tornado.ioloop.IOLoop.instance() + + http_server = tornado.httpserver.HTTPServer(self.app) + http_server.listen(self.options.wport) + + tornado.ioloop.PeriodicCallback(self.tick, 5).start() + try: + iol.start() + except (Stop, KeyboardInterrupt): + self.shutdown() + + def _process_flow(self, f): + if self.state.intercept and self.state.intercept( + f) and not f.request.is_replay: + f.intercept(self) + else: + f.reply() + + def handle_request(self, f): + super(WebMaster, self).handle_request(f) + self._process_flow(f) + + def handle_response(self, f): + super(WebMaster, self).handle_response(f) + self._process_flow(f) + + def handle_error(self, f): + super(WebMaster, self).handle_error(f) + self._process_flow(f) + + def add_event(self, e, level="info"): + super(WebMaster, self).add_event(e, level) + self.state.add_event(e, level) diff --git a/mitmproxy/web/app.py b/mitmproxy/web/app.py new file mode 100644 index 00000000..63b7bf1a --- /dev/null +++ b/mitmproxy/web/app.py @@ -0,0 +1,338 @@ +import os.path +import re +import tornado.web +import tornado.websocket +import logging +import json +import base64 + +from netlib.http import CONTENT_MISSING +from .. import version, filt + + +def _strip_content(flow_state): + """ + Remove flow message content and cert to save transmission space. + + Args: + flow_state: The original flow state. Will be left unmodified + """ + for attr in ("request", "response"): + if attr in flow_state: + message = flow_state[attr] + if message is None: + continue + if message["content"]: + message["contentLength"] = len(message["content"]) + elif message["content"] == CONTENT_MISSING: + message["contentLength"] = None + else: + message["contentLength"] = 0 + del message["content"] + + if "backup" in flow_state: + del flow_state["backup"] + flow_state["modified"] = True + + flow_state.get("server_conn", {}).pop("cert", None) + + return flow_state + + +class APIError(tornado.web.HTTPError): + pass + + +class BasicAuth(object): + + def set_auth_headers(self): + self.set_status(401) + self.set_header('WWW-Authenticate', 'Basic realm=MITMWeb') + self._transforms = [] + self.finish() + + def prepare(self): + wauthenticator = self.application.settings['wauthenticator'] + if wauthenticator: + auth_header = self.request.headers.get('Authorization') + if auth_header is None or not auth_header.startswith('Basic '): + self.set_auth_headers() + else: + self.auth_decoded = base64.decodestring(auth_header[6:]) + self.username, self.password = self.auth_decoded.split(':', 2) + if not wauthenticator.test(self.username, self.password): + self.set_auth_headers() + raise APIError(401, "Invalid username or password.") + + +class RequestHandler(BasicAuth, tornado.web.RequestHandler): + + def set_default_headers(self): + super(RequestHandler, self).set_default_headers() + self.set_header("Server", version.NAMEVERSION) + self.set_header("X-Frame-Options", "DENY") + self.add_header("X-XSS-Protection", "1; mode=block") + self.add_header("X-Content-Type-Options", "nosniff") + self.add_header( + "Content-Security-Policy", + "default-src 'self'; " + "connect-src 'self' ws://* ; " + "style-src 'self' 'unsafe-inline'" + ) + + @property + def json(self): + if not self.request.headers.get("Content-Type").startswith("application/json"): + return None + return json.loads(self.request.body) + + @property + def state(self): + return self.application.master.state + + @property + def master(self): + return self.application.master + + @property + def flow(self): + flow_id = str(self.path_kwargs["flow_id"]) + flow = self.state.flows.get(flow_id) + if flow: + return flow + else: + raise APIError(400, "Flow not found.") + + def write_error(self, status_code, **kwargs): + if "exc_info" in kwargs and isinstance(kwargs["exc_info"][1], APIError): + self.finish(kwargs["exc_info"][1].log_message) + else: + super(RequestHandler, self).write_error(status_code, **kwargs) + + +class IndexHandler(RequestHandler): + + def get(self): + _ = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645 + self.render("index.html") + + +class FiltHelp(RequestHandler): + + def get(self): + self.write(dict( + commands=filt.help + )) + + +class WebSocketEventBroadcaster(BasicAuth, tornado.websocket.WebSocketHandler): + # raise an error if inherited class doesn't specify its own instance. + connections = None + + def open(self): + self.connections.add(self) + + def on_close(self): + self.connections.remove(self) + + @classmethod + def broadcast(cls, **kwargs): + message = json.dumps(kwargs, ensure_ascii=False) + + for conn in cls.connections: + try: + conn.write_message(message) + except: + logging.error("Error sending message", exc_info=True) + + +class ClientConnection(WebSocketEventBroadcaster): + connections = set() + + +class Flows(RequestHandler): + + def get(self): + self.write(dict( + data=[_strip_content(f.get_state()) for f in self.state.flows] + )) + + +class ClearAll(RequestHandler): + + def post(self): + self.state.clear() + + +class AcceptFlows(RequestHandler): + + def post(self): + self.state.flows.accept_all(self.master) + + +class AcceptFlow(RequestHandler): + + def post(self, flow_id): + self.flow.accept_intercept(self.master) + + +class FlowHandler(RequestHandler): + + def delete(self, flow_id): + self.flow.kill(self.master) + self.state.delete_flow(self.flow) + + def put(self, flow_id): + flow = self.flow + flow.backup() + for a, b in self.json.iteritems(): + + if a == "request": + request = flow.request + for k, v in b.iteritems(): + if k in ["method", "scheme", "host", "path", "http_version"]: + setattr(request, k, str(v)) + elif k == "port": + request.port = int(v) + elif k == "headers": + request.headers.set_state(v) + else: + print "Warning: Unknown update {}.{}: {}".format(a, k, v) + + elif a == "response": + response = flow.response + for k, v in b.iteritems(): + if k == "msg": + response.msg = str(v) + elif k == "code": + response.status_code = int(v) + elif k == "http_version": + response.http_version = str(v) + elif k == "headers": + response.headers.set_state(v) + else: + print "Warning: Unknown update {}.{}: {}".format(a, k, v) + else: + print "Warning: Unknown update {}: {}".format(a, b) + self.state.update_flow(flow) + + +class DuplicateFlow(RequestHandler): + + def post(self, flow_id): + self.master.duplicate_flow(self.flow) + + +class RevertFlow(RequestHandler): + + def post(self, flow_id): + self.state.revert(self.flow) + + +class ReplayFlow(RequestHandler): + + def post(self, flow_id): + self.flow.backup() + self.flow.response = None + self.state.update_flow(self.flow) + + r = self.master.replay_request(self.flow) + if r: + raise APIError(400, r) + + +class FlowContent(RequestHandler): + + def get(self, flow_id, message): + message = getattr(self.flow, message) + + if not message.content: + raise APIError(400, "No content.") + + content_encoding = message.headers.get("Content-Encoding", None) + if content_encoding: + content_encoding = re.sub(r"[^\w]", "", content_encoding) + self.set_header("Content-Encoding", content_encoding) + + original_cd = message.headers.get("Content-Disposition", None) + filename = None + if original_cd: + filename = re.search("filename=([\w\" \.\-\(\)]+)", original_cd) + if filename: + filename = filename.group(1) + if not filename: + filename = self.flow.request.path.split("?")[0].split("/")[-1] + + filename = re.sub(r"[^\w\" \.\-\(\)]", "", filename) + cd = "attachment; filename={}".format(filename) + self.set_header("Content-Disposition", cd) + self.set_header("Content-Type", "application/text") + self.set_header("X-Content-Type-Options", "nosniff") + self.set_header("X-Frame-Options", "DENY") + self.write(message.content) + + +class Events(RequestHandler): + + def get(self): + self.write(dict( + data=list(self.state.events) + )) + + +class Settings(RequestHandler): + + def get(self): + self.write(dict( + data=dict( + version=version.VERSION, + mode=str(self.master.server.config.mode), + intercept=self.state.intercept_txt + ) + )) + + def put(self): + update = {} + for k, v in self.json.iteritems(): + if k == "intercept": + self.state.set_intercept(v) + update[k] = v + else: + print("Warning: Unknown setting {}: {}".format(k, v)) + + ClientConnection.broadcast( + type="settings", + cmd="update", + data=update + ) + + +class Application(tornado.web.Application): + + def __init__(self, master, debug, wauthenticator): + self.master = master + handlers = [ + (r"/", IndexHandler), + (r"/filter-help", FiltHelp), + (r"/updates", ClientConnection), + (r"/events", Events), + (r"/flows", Flows), + (r"/flows/accept", AcceptFlows), + (r"/flows/(?P[0-9a-f\-]+)", FlowHandler), + (r"/flows/(?P[0-9a-f\-]+)/accept", AcceptFlow), + (r"/flows/(?P[0-9a-f\-]+)/duplicate", DuplicateFlow), + (r"/flows/(?P[0-9a-f\-]+)/replay", ReplayFlow), + (r"/flows/(?P[0-9a-f\-]+)/revert", RevertFlow), + (r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response)/content", FlowContent), + (r"/settings", Settings), + (r"/clear", ClearAll), + ] + settings = dict( + template_path=os.path.join(os.path.dirname(__file__), "templates"), + static_path=os.path.join(os.path.dirname(__file__), "static"), + xsrf_cookies=True, + cookie_secret=os.urandom(256), + debug=debug, + wauthenticator=wauthenticator, + ) + super(Application, self).__init__(handlers, **settings) diff --git a/mitmproxy/web/static/app.css b/mitmproxy/web/static/app.css new file mode 100644 index 00000000..94a6abf0 --- /dev/null +++ b/mitmproxy/web/static/app.css @@ -0,0 +1,421 @@ +html { + box-sizing: border-box; +} +*, +*:before, +*:after { + box-sizing: inherit; +} +.resource-icon { + width: 32px; + height: 32px; +} +.resource-icon-css { + background-image: url(images/chrome-devtools/resourceCSSIcon.png); +} +.resource-icon-document { + background-image: url(images/chrome-devtools/resourceDocumentIcon.png); +} +.resource-icon-js { + background-image: url(images/chrome-devtools/resourceJSIcon.png); +} +.resource-icon-plain { + background-image: url(images/chrome-devtools/resourcePlainIcon.png); +} +.resource-icon-executable { + background-image: url(images/resourceExecutableIcon.png); +} +.resource-icon-flash { + background-image: url(images/resourceFlashIcon.png); +} +.resource-icon-image { + background-image: url(images/resourceImageIcon.png); +} +.resource-icon-java { + background-image: url(images/resourceJavaIcon.png); +} +.resource-icon-not-modified { + background-image: url(images/resourceNotModifiedIcon.png); +} +.resource-icon-redirect { + background-image: url(images/resourceRedirectIcon.png); +} +html, +body, +#container { + height: 100%; + margin: 0; + overflow: hidden; +} +#container { + display: flex; + flex-direction: column; + outline: none; +} +#container > header, +#container > footer, +#container > .eventlog { + flex: 0 0 auto; +} +.main-view { + flex: 1 1 auto; + height: 0; + display: flex; + flex-direction: row; +} +.main-view.vertical { + flex-direction: column; +} +.main-view .flow-detail, +.main-view .flow-table { + flex: 1 1 auto; +} +.splitter { + flex: 0 0 1px; + background-color: #aaa; + position: relative; +} +.splitter > div { + position: absolute; +} +.splitter.splitter-x { + cursor: col-resize; +} +.splitter.splitter-x > div { + margin-left: -1px; + width: 4px; + height: 100%; +} +.splitter.splitter-y { + cursor: row-resize; +} +.splitter.splitter-y > div { + margin-top: -1px; + height: 4px; + width: 100%; +} +.nav-tabs { + border-bottom: solid #a6a6a6 1px; +} +.nav-tabs a { + display: inline-block; + border: solid transparent 1px; + text-decoration: none; +} +.nav-tabs a.active { + background-color: white; + border-color: #a6a6a6; + border-bottom-color: white; +} +.nav-tabs a.special { + color: white; + background-color: #396cad; + border-bottom-color: #396cad; +} +.nav-tabs a.special:hover { + background-color: #5386c6; +} +.nav-tabs-lg a { + padding: 3px 14px; + margin: 0 2px -1px; +} +.nav-tabs-sm a { + padding: 0px 7px; + margin: 2px 2px -1px; +} +.nav-tabs-sm a.nav-action { + float: right; + padding: 0; + margin: 1px 0 0px; +} +header { + padding-top: 0.5em; + background-color: white; +} +header .menu { + padding: 10px; + border-bottom: solid #a6a6a6 1px; +} +.menu-row { + margin-left: -2px; + margin-right: -3px; +} +.filter-input { + position: relative; + min-height: 1px; + padding-left: 2.5px; + padding-right: 2.5px; +} +@media (min-width: 768px) { + .filter-input { + float: left; + width: 25%; + } +} +.filter-input .popover { + top: 27px; + display: block; + max-width: none; + opacity: 0.9; +} +.filter-input .popover .popover-content { + max-height: 500px; + overflow-y: auto; +} +.flow-table { + width: 100%; + overflow: auto; +} +.flow-table table { + width: 100%; + table-layout: fixed; +} +.flow-table thead { + background-color: #F2F2F2; + line-height: 23px; +} +.flow-table th { + font-weight: normal; + box-shadow: 0 1px 0 #a6a6a6; + position: relative !important; + padding-left: 1px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.flow-table th.sort-asc, +.flow-table th.sort-desc { + background-color: #fafafa; +} +.flow-table th.sort-asc:after, +.flow-table th.sort-desc:after { + font: normal normal normal 14px/1 FontAwesome; + position: absolute; + right: 3px; + top: 3px; + padding: 2px; + background-color: rgba(250, 250, 250, 0.8); +} +.flow-table th.sort-asc:after { + content: "\f0de"; +} +.flow-table th.sort-desc:after { + content: "\f0dd"; +} +.flow-table tr { + cursor: pointer; +} +.flow-table tr:nth-child(even) { + background-color: rgba(0, 0, 0, 0.05); +} +.flow-table tr.selected { + background-color: rgba(193, 215, 235, 0.5) !important; +} +.flow-table tr.highlighted { + background-color: rgba(255, 204, 0, 0.4); +} +.flow-table tr.highlighted:nth-child(even) { + background-color: rgba(255, 204, 0, 0.5); +} +.flow-table td { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.flow-table tr.intercepted:not(.has-response) .col-path, +.flow-table tr.intercepted:not(.has-response) .col-method { + color: #ff8000; +} +.flow-table tr.intercepted.has-response .col-status, +.flow-table tr.intercepted.has-response .col-size, +.flow-table tr.intercepted.has-response .col-time { + color: #ff8000; +} +.flow-table .fa { + line-height: inherit; +} +.flow-table .fa.pull-right { + margin-left: 0; +} +.flow-table .col-tls { + width: 10px; +} +.flow-table .col-tls-https { + background-color: rgba(0, 185, 0, 0.5); +} +.flow-table .col-icon { + width: 32px; +} +.flow-table .col-path .fa-repeat { + color: green; +} +.flow-table .col-path .fa-pause { + color: #ff8000; +} +.flow-table .col-method { + width: 60px; +} +.flow-table .col-status { + width: 50px; +} +.flow-table .col-size { + width: 70px; +} +.flow-table .col-time { + width: 50px; +} +.flow-table td.col-time, +.flow-table td.col-size { + text-align: right; +} +.flow-detail { + width: 100%; + overflow-x: auto; + overflow-y: scroll; + /*.request .response-line, + .response .request-line { + opacity: 0.7; + }*/ +} +.flow-detail nav { + background-color: #F2F2F2; +} +.flow-detail section { + padding: 5px 12px; +} +.flow-detail .first-line { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + background-color: #428bca; + color: white; + margin: 0 -8px; + padding: 4px 8px; + border-radius: 5px; + word-break: break-all; + max-height: 100px; + overflow-y: auto; +} +.flow-detail .request-line { + margin-bottom: 2px; +} +.flow-detail hr { + margin: 0 0 5px; +} +.inline-input { + margin: 0 -5px; + padding: 0 5px; +} +.inline-input[contenteditable] { + background-color: rgba(255, 255, 255, 0.2); +} +.inline-input[contenteditable].has-warning { + color: #ffb8b8; +} +.view-options { + margin-top: 10px; +} +.flow-detail table { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + width: 100%; + table-layout: fixed; + word-break: break-all; +} +.flow-detail table tr:not(:first-child) { + border-top: 1px solid #f7f7f7; +} +.flow-detail table td { + vertical-align: top; +} +.connection-table td:first-child { + width: 50%; + padding-right: 1em; +} +.header-table td { + line-height: 1.3em; +} +.header-table .header-name { + width: 33%; + padding-right: 1em; +} +.connection-table td, +.timing-table td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.flowview-image { + text-align: center; +} +.flowview-image img { + max-width: 100%; + max-height: 100%; +} +.prompt-dialog { + top: 0; + bottom: 0; + left: 0; + right: 0; + position: fixed; + z-index: 100; + background-color: rgba(0, 0, 0, 0.1); +} +.prompt-content { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 25px; + padding: 2px 5px; + background-color: white; + box-shadow: 0 -1px 3px lightgray; +} +.prompt-content .option { + cursor: pointer; +} +.prompt-content .option:not(:last-child)::after { + content: ", "; +} +.eventlog { + height: 200px; + flex: 0 0 auto; + display: flex; + flex-direction: column; +} +.eventlog > div { + background-color: #F2F2F2; + padding: 0 5px; + flex: 0 0 auto; +} +.eventlog > pre { + flex: 1 1 auto; + margin: 0; + border-radius: 0; + overflow-x: auto; + overflow-y: scroll; + background-color: #fcfcfc; +} +.eventlog .fa-close { + cursor: pointer; + float: right; + color: grey; + padding: 3px 0; + padding-left: 10px; +} +.eventlog .fa-close:hover { + color: black; +} +.eventlog .label { + cursor: pointer; + vertical-align: middle; + display: inline-block; + margin-top: -2px; + margin-left: 3px; +} +footer { + box-shadow: 0 -1px 3px lightgray; + padding: 0px 10px 3px; +} + +/*# sourceMappingURL=app.css.map */ diff --git a/mitmproxy/web/static/app.js b/mitmproxy/web/static/app.js new file mode 100644 index 00000000..27f356f7 --- /dev/null +++ b/mitmproxy/web/static/app.js @@ -0,0 +1,6271 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (isFunction(emitter._events[type])) + ret = 1; + else + ret = emitter._events[type].length; + return ret; +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],2:[function(require,module,exports){ +"use strict"; + +var $ = require("jquery"); +var _ = require("lodash"); +var AppDispatcher = require("./dispatcher.js").AppDispatcher; + +var ActionTypes = { + // Connection + CONNECTION_OPEN: "connection_open", + CONNECTION_CLOSE: "connection_close", + CONNECTION_ERROR: "connection_error", + + // Stores + SETTINGS_STORE: "settings", + EVENT_STORE: "events", + FLOW_STORE: "flows" +}; + +var StoreCmds = { + ADD: "add", + UPDATE: "update", + REMOVE: "remove", + RESET: "reset" +}; + +var ConnectionActions = { + open: function open() { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_OPEN + }); + }, + close: function close() { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_CLOSE + }); + }, + error: function error() { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_ERROR + }); + } +}; + +var SettingsActions = { + update: function update(settings) { + + $.ajax({ + type: "PUT", + url: "/settings", + contentType: 'application/json', + data: JSON.stringify(settings) + }); + + /* + //Facebook Flux: We do an optimistic update on the client already. + AppDispatcher.dispatchViewAction({ + type: ActionTypes.SETTINGS_STORE, + cmd: StoreCmds.UPDATE, + data: settings + }); + */ + } +}; + +var EventLogActions_event_id = 0; +var EventLogActions = { + add_event: function add_event(message) { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.EVENT_STORE, + cmd: StoreCmds.ADD, + data: { + message: message, + level: "web", + id: "viewAction-" + EventLogActions_event_id++ + } + }); + } +}; + +var FlowActions = { + accept: function accept(flow) { + $.post("/flows/" + flow.id + "/accept"); + }, + accept_all: function accept_all() { + $.post("/flows/accept"); + }, + "delete": function _delete(flow) { + $.ajax({ + type: "DELETE", + url: "/flows/" + flow.id + }); + }, + duplicate: function duplicate(flow) { + $.post("/flows/" + flow.id + "/duplicate"); + }, + replay: function replay(flow) { + $.post("/flows/" + flow.id + "/replay"); + }, + revert: function revert(flow) { + $.post("/flows/" + flow.id + "/revert"); + }, + update: function update(flow, nextProps) { + /* + //Facebook Flux: We do an optimistic update on the client already. + var nextFlow = _.cloneDeep(flow); + _.merge(nextFlow, nextProps); + AppDispatcher.dispatchViewAction({ + type: ActionTypes.FLOW_STORE, + cmd: StoreCmds.UPDATE, + data: nextFlow + }); + */ + $.ajax({ + type: "PUT", + url: "/flows/" + flow.id, + contentType: 'application/json', + data: JSON.stringify(nextProps) + }); + }, + clear: function clear() { + $.post("/clear"); + } +}; + +var Query = { + SEARCH: "s", + HIGHLIGHT: "h", + SHOW_EVENTLOG: "e" +}; + +module.exports = { + ActionTypes: ActionTypes, + ConnectionActions: ConnectionActions, + FlowActions: FlowActions, + StoreCmds: StoreCmds, + SettingsActions: SettingsActions, + EventLogActions: EventLogActions, + Query: Query +}; + + +},{"./dispatcher.js":21,"jquery":"jquery","lodash":"lodash"}],3:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var ReactRouter = require("react-router"); +var $ = require("jquery"); +var Connection = require("./connection"); +var proxyapp = require("./components/proxyapp.js"); +var EventLogActions = require("./actions.js").EventLogActions; + +$(function () { + window.ws = new Connection("/updates"); + + window.onerror = function (msg) { + EventLogActions.add_event(msg); + }; + + ReactRouter.run(proxyapp.routes, function (Handler, state) { + React.render(React.createElement(Handler, null), document.body); + }); +}); + + +},{"./actions.js":2,"./components/proxyapp.js":18,"./connection":20,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var ReactRouter = require("react-router"); +var _ = require("lodash"); + +// http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html (also contains inverse example) +var AutoScrollMixin = { + componentWillUpdate: function componentWillUpdate() { + var node = this.getDOMNode(); + this._shouldScrollBottom = node.scrollTop !== 0 && node.scrollTop + node.clientHeight === node.scrollHeight; + }, + componentDidUpdate: function componentDidUpdate() { + if (this._shouldScrollBottom) { + var node = this.getDOMNode(); + node.scrollTop = node.scrollHeight; + } + } +}; + +var StickyHeadMixin = { + adjustHead: function adjustHead() { + // Abusing CSS transforms to set the element + // referenced as head into some kind of position:sticky. + var head = this.refs.head.getDOMNode(); + head.style.transform = "translate(0," + this.getDOMNode().scrollTop + "px)"; + } +}; + +var SettingsState = { + contextTypes: { + settingsStore: React.PropTypes.object.isRequired + }, + getInitialState: function getInitialState() { + return { + settings: this.context.settingsStore.dict + }; + }, + componentDidMount: function componentDidMount() { + this.context.settingsStore.addListener("recalculate", this.onSettingsChange); + }, + componentWillUnmount: function componentWillUnmount() { + this.context.settingsStore.removeListener("recalculate", this.onSettingsChange); + }, + onSettingsChange: function onSettingsChange() { + this.setState({ + settings: this.context.settingsStore.dict + }); + } +}; + +var ChildFocus = { + contextTypes: { + returnFocus: React.PropTypes.func + }, + returnFocus: function returnFocus() { + React.findDOMNode(this).blur(); + window.getSelection().removeAllRanges(); + this.context.returnFocus(); + } +}; + +var Navigation = _.extend({}, ReactRouter.Navigation, { + setQuery: function setQuery(dict) { + var q = this.context.router.getCurrentQuery(); + for (var i in dict) { + if (dict.hasOwnProperty(i)) { + q[i] = dict[i] || undefined; //falsey values shall be removed. + } + } + this.replaceWith(this.context.router.getCurrentPath(), this.context.router.getCurrentParams(), q); + }, + replaceWith: function replaceWith(routeNameOrPath, params, query) { + if (routeNameOrPath === undefined) { + routeNameOrPath = this.context.router.getCurrentPath(); + } + if (params === undefined) { + params = this.context.router.getCurrentParams(); + } + if (query === undefined) { + query = this.context.router.getCurrentQuery(); + } + + this.context.router.replaceWith(routeNameOrPath, params, query); + } +}); + +// react-router is fairly good at changing its API regularly. +// We keep the old method for now - if it should turn out that their changes are permanent, +// we may remove this mixin and access react-router directly again. +var RouterState = _.extend({}, ReactRouter.State, { + getQuery: function getQuery() { + // For whatever reason, react-router always returns the same object, which makes comparing + // the current props with nextProps impossible. As a workaround, we just clone the query object. + return _.clone(this.context.router.getCurrentQuery()); + }, + getParams: function getParams() { + return _.clone(this.context.router.getCurrentParams()); + } +}); + +var Splitter = React.createClass({ + displayName: "Splitter", + + getDefaultProps: function getDefaultProps() { + return { + axis: "x" + }; + }, + getInitialState: function getInitialState() { + return { + applied: false, + startX: false, + startY: false + }; + }, + onMouseDown: function onMouseDown(e) { + this.setState({ + startX: e.pageX, + startY: e.pageY + }); + window.addEventListener("mousemove", this.onMouseMove); + window.addEventListener("mouseup", this.onMouseUp); + // Occasionally, only a dragEnd event is triggered, but no mouseUp. + window.addEventListener("dragend", this.onDragEnd); + }, + onDragEnd: function onDragEnd() { + this.getDOMNode().style.transform = ""; + window.removeEventListener("dragend", this.onDragEnd); + window.removeEventListener("mouseup", this.onMouseUp); + window.removeEventListener("mousemove", this.onMouseMove); + }, + onMouseUp: function onMouseUp(e) { + this.onDragEnd(); + + var node = this.getDOMNode(); + var prev = node.previousElementSibling; + var next = node.nextElementSibling; + + var dX = e.pageX - this.state.startX; + var dY = e.pageY - this.state.startY; + var flexBasis; + if (this.props.axis === "x") { + flexBasis = prev.offsetWidth + dX; + } else { + flexBasis = prev.offsetHeight + dY; + } + + prev.style.flex = "0 0 " + Math.max(0, flexBasis) + "px"; + next.style.flex = "1 1 auto"; + + this.setState({ + applied: true + }); + this.onResize(); + }, + onMouseMove: function onMouseMove(e) { + var dX = 0, + dY = 0; + if (this.props.axis === "x") { + dX = e.pageX - this.state.startX; + } else { + dY = e.pageY - this.state.startY; + } + this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)"; + }, + onResize: function onResize() { + // Trigger a global resize event. This notifies components that employ virtual scrolling + // that their viewport may have changed. + window.setTimeout(function () { + window.dispatchEvent(new CustomEvent("resize")); + }, 1); + }, + reset: function reset(willUnmount) { + if (!this.state.applied) { + return; + } + var node = this.getDOMNode(); + var prev = node.previousElementSibling; + var next = node.nextElementSibling; + + prev.style.flex = ""; + next.style.flex = ""; + + if (!willUnmount) { + this.setState({ + applied: false + }); + } + this.onResize(); + }, + componentWillUnmount: function componentWillUnmount() { + this.reset(true); + }, + render: function render() { + var className = "splitter"; + if (this.props.axis === "x") { + className += " splitter-x"; + } else { + className += " splitter-y"; + } + return React.createElement( + "div", + { className: className }, + React.createElement("div", { onMouseDown: this.onMouseDown, draggable: "true" }) + ); + } +}); + +module.exports = { + ChildFocus: ChildFocus, + RouterState: RouterState, + Navigation: Navigation, + StickyHeadMixin: StickyHeadMixin, + AutoScrollMixin: AutoScrollMixin, + Splitter: Splitter, + SettingsState: SettingsState +}; + + +},{"lodash":"lodash","react":"react","react-router":"react-router"}],5:[function(require,module,exports){ +"use strict"; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var React = require("react"); +var common = require("./common.js"); +var utils = require("../utils.js"); + +var contentToHtml = function contentToHtml(content) { + return _.escape(content); +}; +var nodeToContent = function nodeToContent(node) { + return node.textContent; +}; + +/* + Basic Editor Functionality + */ +var EditorBase = React.createClass({ + displayName: "EditorBase", + + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + contentToHtml: React.PropTypes.func, + nodeToContent: React.PropTypes.func, // content === nodeToContent( Node ) + onStop: React.PropTypes.func, + submitOnEnter: React.PropTypes.bool, + className: React.PropTypes.string, + tag: React.PropTypes.string + }, + getDefaultProps: function getDefaultProps() { + return { + contentToHtml: contentToHtml, + nodeToContent: nodeToContent, + submitOnEnter: true, + className: "", + tag: "div" + }; + }, + getInitialState: function getInitialState() { + return { + editable: false + }; + }, + render: function render() { + var className = "inline-input " + this.props.className; + var html = { __html: this.props.contentToHtml(this.props.content) }; + var Tag = this.props.tag; + return React.createElement(Tag, _extends({}, this.props, { + tabIndex: "0", + className: className, + contentEditable: this.state.editable || undefined, // workaround: use undef instead of false to remove attr + onFocus: this.onFocus, + onMouseDown: this.onMouseDown, + onClick: this.onClick, + onBlur: this._stop, + onKeyDown: this.onKeyDown, + onInput: this.onInput, + onPaste: this.onPaste, + dangerouslySetInnerHTML: html + })); + }, + onPaste: function onPaste(e) { + e.preventDefault(); + var content = e.clipboardData.getData("text/plain"); + document.execCommand("insertHTML", false, content); + }, + onMouseDown: function onMouseDown(e) { + this._mouseDown = true; + window.addEventListener("mouseup", this.onMouseUp); + this.props.onMouseDown && this.props.onMouseDown(e); + }, + onMouseUp: function onMouseUp() { + if (this._mouseDown) { + this._mouseDown = false; + window.removeEventListener("mouseup", this.onMouseUp); + } + }, + onClick: function onClick(e) { + this.onMouseUp(); + this.onFocus(e); + }, + onFocus: function onFocus(e) { + console.log("onFocus", this._mouseDown, this._ignore_events, this.state.editable); + if (this._mouseDown || this._ignore_events || this.state.editable) { + return; + } + + //contenteditable in FireFox is more or less broken. + // - we need to blur() and then focus(), otherwise the caret is not shown. + // - blur() + focus() == we need to save the caret position before + // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint + var sel = window.getSelection(); + var range; + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0); + } else if (document.caretPositionFromPoint && e.clientX && e.clientY) { + var pos = document.caretPositionFromPoint(e.clientX, e.clientY); + range = document.createRange(); + range.setStart(pos.offsetNode, pos.offset); + } else if (document.caretRangeFromPoint && e.clientX && e.clientY) { + range = document.caretRangeFromPoint(e.clientX, e.clientY); + } else { + range = document.createRange(); + range.selectNodeContents(React.findDOMNode(this)); + } + + this._ignore_events = true; + this.setState({ editable: true }, function () { + var node = React.findDOMNode(this); + node.blur(); + node.focus(); + this._ignore_events = false; + //sel.removeAllRanges(); + //sel.addRange(range); + }); + }, + stop: function stop() { + // a stop would cause a blur as a side-effect. + // but a blur event must trigger a stop as well. + // to fix this, make stop = blur and do the actual stop in the onBlur handler. + React.findDOMNode(this).blur(); + this.props.onStop && this.props.onStop(); + }, + _stop: function _stop(e) { + if (this._ignore_events) { + return; + } + console.log("_stop", _.extend({}, e)); + window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur + var node = React.findDOMNode(this); + var content = this.props.nodeToContent(node); + this.setState({ editable: false }); + this.props.onDone(content); + this.props.onBlur && this.props.onBlur(e); + }, + reset: function reset() { + React.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content); + }, + onKeyDown: function onKeyDown(e) { + e.stopPropagation(); + switch (e.keyCode) { + case utils.Key.ESC: + e.preventDefault(); + this.reset(); + this.stop(); + break; + case utils.Key.ENTER: + if (this.props.submitOnEnter && !e.shiftKey) { + e.preventDefault(); + this.stop(); + } + break; + default: + break; + } + }, + onInput: function onInput() { + var node = React.findDOMNode(this); + var content = this.props.nodeToContent(node); + this.props.onInput && this.props.onInput(content); + } +}); + +/* + Add Validation to EditorBase + */ +var ValidateEditor = React.createClass({ + displayName: "ValidateEditor", + + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + onInput: React.PropTypes.func, + isValid: React.PropTypes.func, + className: React.PropTypes.string + }, + getInitialState: function getInitialState() { + return { + currentContent: this.props.content + }; + }, + componentWillReceiveProps: function componentWillReceiveProps() { + this.setState({ currentContent: this.props.content }); + }, + onInput: function onInput(content) { + this.setState({ currentContent: content }); + this.props.onInput && this.props.onInput(content); + }, + render: function render() { + var className = this.props.className || ""; + if (this.props.isValid) { + if (this.props.isValid(this.state.currentContent)) { + className += " has-success"; + } else { + className += " has-warning"; + } + } + return React.createElement(EditorBase, _extends({}, this.props, { + ref: "editor", + className: className, + onDone: this.onDone, + onInput: this.onInput + })); + }, + onDone: function onDone(content) { + if (this.props.isValid && !this.props.isValid(content)) { + this.refs.editor.reset(); + content = this.props.content; + } + this.props.onDone(content); + } +}); + +/* + Text Editor with mitmweb-specific convenience features + */ +var ValueEditor = React.createClass({ + displayName: "ValueEditor", + + mixins: [common.ChildFocus], + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + inline: React.PropTypes.bool + }, + render: function render() { + var tag = this.props.inline ? "span" : "div"; + return React.createElement(ValidateEditor, _extends({}, this.props, { + onStop: this.onStop, + tag: tag + })); + }, + focus: function focus() { + React.findDOMNode(this).focus(); + }, + onStop: function onStop() { + this.returnFocus(); + } +}); + +module.exports = { + ValueEditor: ValueEditor +}; + + +},{"../utils.js":26,"./common.js":4,"react":"react"}],6:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var common = require("./common.js"); +var Query = require("../actions.js").Query; +var VirtualScrollMixin = require("./virtualscroll.js"); +var views = require("../store/view.js"); +var _ = require("lodash"); + +var LogMessage = React.createClass({ + displayName: "LogMessage", + + render: function render() { + var entry = this.props.entry; + var indicator; + switch (entry.level) { + case "web": + indicator = React.createElement("i", { className: "fa fa-fw fa-html5" }); + break; + case "debug": + indicator = React.createElement("i", { className: "fa fa-fw fa-bug" }); + break; + default: + indicator = React.createElement("i", { className: "fa fa-fw fa-info" }); + } + return React.createElement( + "div", + null, + indicator, + " ", + entry.message + ); + }, + shouldComponentUpdate: function shouldComponentUpdate() { + return false; // log entries are immutable. + } +}); + +var EventLogContents = React.createClass({ + displayName: "EventLogContents", + + contextTypes: { + eventStore: React.PropTypes.object.isRequired + }, + mixins: [common.AutoScrollMixin, VirtualScrollMixin], + getInitialState: function getInitialState() { + var filterFn = function filterFn(entry) { + return this.props.filter[entry.level]; + }; + var view = new views.StoreView(this.context.eventStore, filterFn.bind(this)); + view.addListener("add", this.onEventLogChange); + view.addListener("recalculate", this.onEventLogChange); + + return { + view: view + }; + }, + componentWillUnmount: function componentWillUnmount() { + this.state.view.close(); + }, + filter: function filter(entry) { + return this.props.filter[entry.level]; + }, + onEventLogChange: function onEventLogChange() { + this.forceUpdate(); + }, + componentWillReceiveProps: function componentWillReceiveProps(nextProps) { + if (nextProps.filter !== this.props.filter) { + this.props.filter = nextProps.filter; // Dirty: Make sure that view filter sees the update. + this.state.view.recalculate(); + } + }, + getDefaultProps: function getDefaultProps() { + return { + rowHeight: 45, + rowHeightMin: 15, + placeholderTagName: "div" + }; + }, + renderRow: function renderRow(elem) { + return React.createElement(LogMessage, { key: elem.id, entry: elem }); + }, + render: function render() { + var entries = this.state.view.list; + var rows = this.renderRows(entries); + + return React.createElement( + "pre", + { onScroll: this.onScroll }, + this.getPlaceholderTop(entries.length), + rows, + this.getPlaceholderBottom(entries.length) + ); + } +}); + +var ToggleFilter = React.createClass({ + displayName: "ToggleFilter", + + toggle: function toggle(e) { + e.preventDefault(); + return this.props.toggleLevel(this.props.name); + }, + render: function render() { + var className = "label "; + if (this.props.active) { + className += "label-primary"; + } else { + className += "label-default"; + } + return React.createElement( + "a", + { + href: "#", + className: className, + onClick: this.toggle }, + this.props.name + ); + } +}); + +var EventLog = React.createClass({ + displayName: "EventLog", + + mixins: [common.Navigation], + getInitialState: function getInitialState() { + return { + filter: { + "debug": false, + "info": true, + "web": true + } + }; + }, + close: function close() { + var d = {}; + d[Query.SHOW_EVENTLOG] = undefined; + this.setQuery(d); + }, + toggleLevel: function toggleLevel(level) { + var filter = _.extend({}, this.state.filter); + filter[level] = !filter[level]; + this.setState({ filter: filter }); + }, + render: function render() { + return React.createElement( + "div", + { className: "eventlog" }, + React.createElement( + "div", + null, + "Eventlog", + React.createElement( + "div", + { className: "pull-right" }, + React.createElement(ToggleFilter, { name: "debug", active: this.state.filter.debug, toggleLevel: this.toggleLevel }), + React.createElement(ToggleFilter, { name: "info", active: this.state.filter.info, toggleLevel: this.toggleLevel }), + React.createElement(ToggleFilter, { name: "web", active: this.state.filter.web, toggleLevel: this.toggleLevel }), + React.createElement("i", { onClick: this.close, className: "fa fa-close" }) + ) + ), + React.createElement(EventLogContents, { filter: this.state.filter }) + ); + } +}); + +module.exports = EventLog; + + +},{"../actions.js":2,"../store/view.js":25,"./common.js":4,"./virtualscroll.js":19,"lodash":"lodash","react":"react"}],7:[function(require,module,exports){ +"use strict"; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var React = require("react"); +var RequestUtils = require("../flow/utils.js").RequestUtils; +var ResponseUtils = require("../flow/utils.js").ResponseUtils; +var utils = require("../utils.js"); + +var TLSColumn = React.createClass({ + displayName: "TLSColumn", + + statics: { + Title: React.createClass({ + displayName: "Title", + + render: function render() { + return React.createElement("th", _extends({}, this.props, { className: "col-tls " + (this.props.className || "") })); + } + }), + sortKeyFun: function sortKeyFun(flow) { + return flow.request.scheme; + } + }, + render: function render() { + var flow = this.props.flow; + var ssl = flow.request.scheme === "https"; + var classes; + if (ssl) { + classes = "col-tls col-tls-https"; + } else { + classes = "col-tls col-tls-http"; + } + return React.createElement("td", { className: classes }); + } +}); + +var IconColumn = React.createClass({ + displayName: "IconColumn", + + statics: { + Title: React.createClass({ + displayName: "Title", + + render: function render() { + return React.createElement("th", _extends({}, this.props, { className: "col-icon " + (this.props.className || "") })); + } + }) + }, + render: function render() { + var flow = this.props.flow; + + var icon; + if (flow.response) { + var contentType = ResponseUtils.getContentType(flow.response); + + //TODO: We should assign a type to the flow somewhere else. + if (flow.response.status_code === 304) { + icon = "resource-icon-not-modified"; + } else if (300 <= flow.response.status_code && flow.response.status_code < 400) { + icon = "resource-icon-redirect"; + } else if (contentType && contentType.indexOf("image") >= 0) { + icon = "resource-icon-image"; + } else if (contentType && contentType.indexOf("javascript") >= 0) { + icon = "resource-icon-js"; + } else if (contentType && contentType.indexOf("css") >= 0) { + icon = "resource-icon-css"; + } else if (contentType && contentType.indexOf("html") >= 0) { + icon = "resource-icon-document"; + } + } + if (!icon) { + icon = "resource-icon-plain"; + } + + icon += " resource-icon"; + return React.createElement( + "td", + { className: "col-icon" }, + React.createElement("div", { className: icon }) + ); + } +}); + +var PathColumn = React.createClass({ + displayName: "PathColumn", + + statics: { + Title: React.createClass({ + displayName: "Title", + + render: function render() { + return React.createElement( + "th", + _extends({}, this.props, { className: "col-path " + (this.props.className || "") }), + "Path" + ); + } + }), + sortKeyFun: function sortKeyFun(flow) { + return RequestUtils.pretty_url(flow.request); + } + }, + render: function render() { + var flow = this.props.flow; + return React.createElement( + "td", + { className: "col-path" }, + flow.request.is_replay ? React.createElement("i", { className: "fa fa-fw fa-repeat pull-right" }) : null, + flow.intercepted ? React.createElement("i", { className: "fa fa-fw fa-pause pull-right" }) : null, + RequestUtils.pretty_url(flow.request) + ); + } +}); + +var MethodColumn = React.createClass({ + displayName: "MethodColumn", + + statics: { + Title: React.createClass({ + displayName: "Title", + + render: function render() { + return React.createElement( + "th", + _extends({}, this.props, { className: "col-method " + (this.props.className || "") }), + "Method" + ); + } + }), + sortKeyFun: function sortKeyFun(flow) { + return flow.request.method; + } + }, + render: function render() { + var flow = this.props.flow; + return React.createElement( + "td", + { className: "col-method" }, + flow.request.method + ); + } +}); + +var StatusColumn = React.createClass({ + displayName: "StatusColumn", + + statics: { + Title: React.createClass({ + displayName: "Title", + + render: function render() { + return React.createElement( + "th", + _extends({}, this.props, { className: "col-status " + (this.props.className || "") }), + "Status" + ); + } + }), + sortKeyFun: function sortKeyFun(flow) { + return flow.response ? flow.response.status_code : undefined; + } + }, + render: function render() { + var flow = this.props.flow; + var status; + if (flow.response) { + status = flow.response.status_code; + } else { + status = null; + } + return React.createElement( + "td", + { className: "col-status" }, + status + ); + } +}); + +var SizeColumn = React.createClass({ + displayName: "SizeColumn", + + statics: { + Title: React.createClass({ + displayName: "Title", + + render: function render() { + return React.createElement( + "th", + _extends({}, this.props, { className: "col-size " + (this.props.className || "") }), + "Size" + ); + } + }), + sortKeyFun: function sortKeyFun(flow) { + var total = flow.request.contentLength; + if (flow.response) { + total += flow.response.contentLength || 0; + } + return total; + } + }, + render: function render() { + var flow = this.props.flow; + + var total = flow.request.contentLength; + if (flow.response) { + total += flow.response.contentLength || 0; + } + var size = utils.formatSize(total); + return React.createElement( + "td", + { className: "col-size" }, + size + ); + } +}); + +var TimeColumn = React.createClass({ + displayName: "TimeColumn", + + statics: { + Title: React.createClass({ + displayName: "Title", + + render: function render() { + return React.createElement( + "th", + _extends({}, this.props, { className: "col-time " + (this.props.className || "") }), + "Time" + ); + } + }), + sortKeyFun: function sortKeyFun(flow) { + if (flow.response) { + return flow.response.timestamp_end - flow.request.timestamp_start; + } + } + }, + render: function render() { + var flow = this.props.flow; + var time; + if (flow.response) { + time = utils.formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); + } else { + time = "..."; + } + return React.createElement( + "td", + { className: "col-time" }, + time + ); + } +}); + +var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, SizeColumn, TimeColumn]; + +module.exports = all_columns; + + +},{"../flow/utils.js":23,"../utils.js":26,"react":"react"}],8:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var common = require("./common.js"); +var utils = require("../utils.js"); +var _ = require("lodash"); + +var VirtualScrollMixin = require("./virtualscroll.js"); +var flowtable_columns = require("./flowtable-columns.js"); + +var FlowRow = React.createClass({ + displayName: "FlowRow", + + render: function render() { + var flow = this.props.flow; + var columns = this.props.columns.map((function (Column) { + return React.createElement(Column, { key: Column.displayName, flow: flow }); + }).bind(this)); + var className = ""; + if (this.props.selected) { + className += " selected"; + } + if (this.props.highlighted) { + className += " highlighted"; + } + if (flow.intercepted) { + className += " intercepted"; + } + if (flow.request) { + className += " has-request"; + } + if (flow.response) { + className += " has-response"; + } + + return React.createElement( + "tr", + { className: className, onClick: this.props.selectFlow.bind(null, flow) }, + columns + ); + }, + shouldComponentUpdate: function shouldComponentUpdate(nextProps) { + return true; + // Further optimization could be done here + // by calling forceUpdate on flow updates, selection changes and column changes. + //return ( + //(this.props.columns.length !== nextProps.columns.length) || + //(this.props.selected !== nextProps.selected) + //); + } +}); + +var FlowTableHead = React.createClass({ + displayName: "FlowTableHead", + + getInitialState: function getInitialState() { + return { + sortColumn: undefined, + sortDesc: false + }; + }, + onClick: function onClick(Column) { + var sortDesc = this.state.sortDesc; + var hasSort = Column.sortKeyFun; + if (Column === this.state.sortColumn) { + sortDesc = !sortDesc; + this.setState({ + sortDesc: sortDesc + }); + } else { + this.setState({ + sortColumn: hasSort && Column, + sortDesc: false + }); + } + var sortKeyFun; + if (!sortDesc) { + sortKeyFun = Column.sortKeyFun; + } else { + sortKeyFun = hasSort && function () { + var k = Column.sortKeyFun.apply(this, arguments); + if (_.isString(k)) { + return utils.reverseString("" + k); + } else { + return -k; + } + }; + } + this.props.setSortKeyFun(sortKeyFun); + }, + render: function render() { + var columns = this.props.columns.map((function (Column) { + var onClick = this.onClick.bind(this, Column); + var className; + if (this.state.sortColumn === Column) { + if (this.state.sortDesc) { + className = "sort-desc"; + } else { + className = "sort-asc"; + } + } + return React.createElement(Column.Title, { + key: Column.displayName, + onClick: onClick, + className: className }); + }).bind(this)); + return React.createElement( + "thead", + null, + React.createElement( + "tr", + null, + columns + ) + ); + } +}); + +var ROW_HEIGHT = 32; + +var FlowTable = React.createClass({ + displayName: "FlowTable", + + mixins: [common.StickyHeadMixin, common.AutoScrollMixin, VirtualScrollMixin], + contextTypes: { + view: React.PropTypes.object.isRequired + }, + getInitialState: function getInitialState() { + return { + columns: flowtable_columns + }; + }, + componentWillMount: function componentWillMount() { + this.context.view.addListener("add", this.onChange); + this.context.view.addListener("update", this.onChange); + this.context.view.addListener("remove", this.onChange); + this.context.view.addListener("recalculate", this.onChange); + }, + componentWillUnmount: function componentWillUnmount() { + this.context.view.removeListener("add", this.onChange); + this.context.view.removeListener("update", this.onChange); + this.context.view.removeListener("remove", this.onChange); + this.context.view.removeListener("recalculate", this.onChange); + }, + getDefaultProps: function getDefaultProps() { + return { + rowHeight: ROW_HEIGHT + }; + }, + onScrollFlowTable: function onScrollFlowTable() { + this.adjustHead(); + this.onScroll(); + }, + onChange: function onChange() { + this.forceUpdate(); + }, + scrollIntoView: function scrollIntoView(flow) { + this.scrollRowIntoView(this.context.view.index(flow), this.refs.body.getDOMNode().offsetTop); + }, + renderRow: function renderRow(flow) { + var selected = flow === this.props.selected; + var highlighted = this.context.view._highlight && this.context.view._highlight[flow.id]; + + return React.createElement(FlowRow, { key: flow.id, + ref: flow.id, + flow: flow, + columns: this.state.columns, + selected: selected, + highlighted: highlighted, + selectFlow: this.props.selectFlow + }); + }, + render: function render() { + var flows = this.context.view.list; + var rows = this.renderRows(flows); + + return React.createElement( + "div", + { className: "flow-table", onScroll: this.onScrollFlowTable }, + React.createElement( + "table", + null, + React.createElement(FlowTableHead, { ref: "head", + columns: this.state.columns, + setSortKeyFun: this.props.setSortKeyFun }), + React.createElement( + "tbody", + { ref: "body" }, + this.getPlaceholderTop(flows.length), + rows, + this.getPlaceholderBottom(flows.length) + ) + ) + ); + } +}); + +module.exports = FlowTable; + + +},{"../utils.js":26,"./common.js":4,"./flowtable-columns.js":7,"./virtualscroll.js":19,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ +"use strict"; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var React = require("react"); +var _ = require("lodash"); + +var MessageUtils = require("../../flow/utils.js").MessageUtils; +var utils = require("../../utils.js"); + +var image_regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i; +var ViewImage = React.createClass({ + displayName: "ViewImage", + + statics: { + matches: function matches(message) { + return image_regex.test(MessageUtils.getContentType(message)); + } + }, + render: function render() { + var url = MessageUtils.getContentURL(this.props.flow, this.props.message); + return React.createElement( + "div", + { className: "flowview-image" }, + React.createElement("img", { src: url, alt: "preview", className: "img-thumbnail" }) + ); + } +}); + +var RawMixin = { + getInitialState: function getInitialState() { + return { + content: undefined, + request: undefined + }; + }, + requestContent: function requestContent(nextProps) { + if (this.state.request) { + this.state.request.abort(); + } + var request = MessageUtils.getContent(nextProps.flow, nextProps.message); + this.setState({ + content: undefined, + request: request + }); + request.done((function (data) { + this.setState({ content: data }); + }).bind(this)).fail((function (jqXHR, textStatus, errorThrown) { + if (textStatus === "abort") { + return; + } + this.setState({ content: "AJAX Error: " + textStatus + "\r\n" + errorThrown }); + }).bind(this)).always((function () { + this.setState({ request: undefined }); + }).bind(this)); + }, + componentWillMount: function componentWillMount() { + this.requestContent(this.props); + }, + componentWillReceiveProps: function componentWillReceiveProps(nextProps) { + if (nextProps.message !== this.props.message) { + this.requestContent(nextProps); + } + }, + componentWillUnmount: function componentWillUnmount() { + if (this.state.request) { + this.state.request.abort(); + } + }, + render: function render() { + if (!this.state.content) { + return React.createElement( + "div", + { className: "text-center" }, + React.createElement("i", { className: "fa fa-spinner fa-spin" }) + ); + } + return this.renderContent(); + } +}; + +var ViewRaw = React.createClass({ + displayName: "ViewRaw", + + mixins: [RawMixin], + statics: { + matches: function matches(message) { + return true; + } + }, + renderContent: function renderContent() { + return React.createElement( + "pre", + null, + this.state.content + ); + } +}); + +var json_regex = /^application\/json$/i; +var ViewJSON = React.createClass({ + displayName: "ViewJSON", + + mixins: [RawMixin], + statics: { + matches: function matches(message) { + return json_regex.test(MessageUtils.getContentType(message)); + } + }, + renderContent: function renderContent() { + var json = this.state.content; + try { + json = JSON.stringify(JSON.parse(json), null, 2); + } catch (e) {} + return React.createElement( + "pre", + null, + json + ); + } +}); + +var ViewAuto = React.createClass({ + displayName: "ViewAuto", + + statics: { + matches: function matches() { + return false; // don't match itself + }, + findView: function findView(message) { + for (var i = 0; i < all.length; i++) { + if (all[i].matches(message)) { + return all[i]; + } + } + return all[all.length - 1]; + } + }, + render: function render() { + var View = ViewAuto.findView(this.props.message); + return React.createElement(View, this.props); + } +}); + +var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw]; + +var ContentEmpty = React.createClass({ + displayName: "ContentEmpty", + + render: function render() { + var message_name = this.props.flow.request === this.props.message ? "request" : "response"; + return React.createElement( + "div", + { className: "alert alert-info" }, + "No ", + message_name, + " content." + ); + } +}); + +var ContentMissing = React.createClass({ + displayName: "ContentMissing", + + render: function render() { + var message_name = this.props.flow.request === this.props.message ? "Request" : "Response"; + return React.createElement( + "div", + { className: "alert alert-info" }, + message_name, + " content missing." + ); + } +}); + +var TooLarge = React.createClass({ + displayName: "TooLarge", + + statics: { + isTooLarge: function isTooLarge(message) { + var max_mb = ViewImage.matches(message) ? 10 : 0.2; + return message.contentLength > 1024 * 1024 * max_mb; + } + }, + render: function render() { + var size = utils.formatSize(this.props.message.contentLength); + return React.createElement( + "div", + { className: "alert alert-warning" }, + React.createElement( + "button", + { onClick: this.props.onClick, className: "btn btn-xs btn-warning pull-right" }, + "Display anyway" + ), + size, + " content size." + ); + } +}); + +var ViewSelector = React.createClass({ + displayName: "ViewSelector", + + render: function render() { + var views = []; + for (var i = 0; i < all.length; i++) { + var view = all[i]; + var className = "btn btn-default"; + if (view === this.props.active) { + className += " active"; + } + var text; + if (view === ViewAuto) { + text = "auto: " + ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view", ""); + } else { + text = view.displayName.toLowerCase().replace("view", ""); + } + views.push(React.createElement( + "button", + { + key: view.displayName, + onClick: this.props.selectView.bind(null, view), + className: className }, + text + )); + } + + return React.createElement( + "div", + { className: "view-selector btn-group btn-group-xs" }, + views + ); + } +}); + +var ContentView = React.createClass({ + displayName: "ContentView", + + getInitialState: function getInitialState() { + return { + displayLarge: false, + View: ViewAuto + }; + }, + propTypes: { + // It may seem a bit weird at the first glance: + // Every view takes the flow and the message as props, e.g. + // + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired + }, + selectView: function selectView(view) { + this.setState({ + View: view + }); + }, + displayLarge: function displayLarge() { + this.setState({ displayLarge: true }); + }, + componentWillReceiveProps: function componentWillReceiveProps(nextProps) { + if (nextProps.message !== this.props.message) { + this.setState(this.getInitialState()); + } + }, + render: function render() { + var message = this.props.message; + if (message.contentLength === 0) { + return React.createElement(ContentEmpty, this.props); + } else if (message.contentLength === null) { + return React.createElement(ContentMissing, this.props); + } else if (!this.state.displayLarge && TooLarge.isTooLarge(message)) { + return React.createElement(TooLarge, _extends({}, this.props, { onClick: this.displayLarge })); + } + + var downloadUrl = MessageUtils.getContentURL(this.props.flow, message); + + return React.createElement( + "div", + null, + React.createElement(this.state.View, this.props), + React.createElement( + "div", + { className: "view-options text-center" }, + React.createElement(ViewSelector, { selectView: this.selectView, active: this.state.View, message: message }), + " ", + React.createElement( + "a", + { className: "btn btn-default btn-xs", href: downloadUrl }, + React.createElement("i", { className: "fa fa-download" }) + ) + ) + ); + } +}); + +module.exports = ContentView; + + +},{"../../flow/utils.js":23,"../../utils.js":26,"lodash":"lodash","react":"react"}],10:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../../utils.js"); + +var TimeStamp = React.createClass({ + displayName: "TimeStamp", + + render: function render() { + + if (!this.props.t) { + //should be return null, but that triggers a React bug. + return React.createElement("tr", null); + } + + var ts = utils.formatTimeStamp(this.props.t); + + var delta; + if (this.props.deltaTo) { + delta = utils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); + delta = React.createElement( + "span", + { className: "text-muted" }, + "(" + delta + ")" + ); + } else { + delta = null; + } + + return React.createElement( + "tr", + null, + React.createElement( + "td", + null, + this.props.title + ":" + ), + React.createElement( + "td", + null, + ts, + " ", + delta + ) + ); + } +}); + +var ConnectionInfo = React.createClass({ + displayName: "ConnectionInfo", + + render: function render() { + var conn = this.props.conn; + var address = conn.address.address.join(":"); + + var sni = React.createElement("tr", { key: "sni" }); //should be null, but that triggers a React bug. + if (conn.sni) { + sni = React.createElement( + "tr", + { key: "sni" }, + React.createElement( + "td", + null, + React.createElement( + "abbr", + { title: "TLS Server Name Indication" }, + "TLS SNI:" + ) + ), + React.createElement( + "td", + null, + conn.sni + ) + ); + } + return React.createElement( + "table", + { className: "connection-table" }, + React.createElement( + "tbody", + null, + React.createElement( + "tr", + { key: "address" }, + React.createElement( + "td", + null, + "Address:" + ), + React.createElement( + "td", + null, + address + ) + ), + sni + ) + ); + } +}); + +var CertificateInfo = React.createClass({ + displayName: "CertificateInfo", + + render: function render() { + //TODO: We should fetch human-readable certificate representation + // from the server + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + + var preStyle = { maxHeight: 100 }; + return React.createElement( + "div", + null, + client_conn.cert ? React.createElement( + "h4", + null, + "Client Certificate" + ) : null, + client_conn.cert ? React.createElement( + "pre", + { style: preStyle }, + client_conn.cert + ) : null, + server_conn.cert ? React.createElement( + "h4", + null, + "Server Certificate" + ) : null, + server_conn.cert ? React.createElement( + "pre", + { style: preStyle }, + server_conn.cert + ) : null + ); + } +}); + +var Timing = React.createClass({ + displayName: "Timing", + + render: function render() { + var flow = this.props.flow; + var sc = flow.server_conn; + var cc = flow.client_conn; + var req = flow.request; + var resp = flow.response; + + var timestamps = [{ + title: "Server conn. initiated", + t: sc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Server conn. TCP handshake", + t: sc.timestamp_tcp_setup, + deltaTo: req.timestamp_start + }, { + title: "Server conn. SSL handshake", + t: sc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "Client conn. established", + t: cc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Client conn. SSL handshake", + t: cc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "First request byte", + t: req.timestamp_start + }, { + title: "Request complete", + t: req.timestamp_end, + deltaTo: req.timestamp_start + }]; + + if (flow.response) { + timestamps.push({ + title: "First response byte", + t: resp.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Response complete", + t: resp.timestamp_end, + deltaTo: req.timestamp_start + }); + } + + //Add unique key for each row. + timestamps.forEach(function (e) { + e.key = e.title; + }); + + timestamps = _.sortBy(timestamps, 't'); + + var rows = timestamps.map(function (e) { + return React.createElement(TimeStamp, e); + }); + + return React.createElement( + "div", + null, + React.createElement( + "h4", + null, + "Timing" + ), + React.createElement( + "table", + { className: "timing-table" }, + React.createElement( + "tbody", + null, + rows + ) + ) + ); + } +}); + +var Details = React.createClass({ + displayName: "Details", + + render: function render() { + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + return React.createElement( + "section", + null, + React.createElement( + "h4", + null, + "Client Connection" + ), + React.createElement(ConnectionInfo, { conn: client_conn }), + React.createElement( + "h4", + null, + "Server Connection" + ), + React.createElement(ConnectionInfo, { conn: server_conn }), + React.createElement(CertificateInfo, { flow: flow }), + React.createElement(Timing, { flow: flow }) + ); + } +}); + +module.exports = Details; + + +},{"../../utils.js":26,"lodash":"lodash","react":"react"}],11:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var Nav = require("./nav.js"); +var Messages = require("./messages.js"); +var Details = require("./details.js"); +var Prompt = require("../prompt.js"); + +var allTabs = { + request: Messages.Request, + response: Messages.Response, + error: Messages.Error, + details: Details +}; + +var FlowView = React.createClass({ + displayName: "FlowView", + + mixins: [common.StickyHeadMixin, common.Navigation, common.RouterState], + getInitialState: function getInitialState() { + return { + prompt: false + }; + }, + getTabs: function getTabs(flow) { + var tabs = []; + ["request", "response", "error"].forEach(function (e) { + if (flow[e]) { + tabs.push(e); + } + }); + tabs.push("details"); + return tabs; + }, + nextTab: function nextTab(i) { + var tabs = this.getTabs(this.props.flow); + var currentIndex = tabs.indexOf(this.getActive()); + // JS modulo operator doesn't correct negative numbers, make sure that we are positive. + var nextIndex = (currentIndex + i + tabs.length) % tabs.length; + this.selectTab(tabs[nextIndex]); + }, + selectTab: function selectTab(panel) { + this.replaceWith("flow", { + flowId: this.getParams().flowId, + detailTab: panel + }); + }, + getActive: function getActive() { + return this.getParams().detailTab; + }, + promptEdit: function promptEdit() { + var options; + switch (this.getActive()) { + case "request": + options = ["method", "url", { text: "http version", key: "v" }, "header" + /*, "content"*/]; + break; + case "response": + options = [{ text: "http version", key: "v" }, "code", "message", "header" + /*, "content"*/]; + break; + case "details": + return; + default: + throw "Unknown tab for edit: " + this.getActive(); + } + + this.setState({ + prompt: { + done: (function (k) { + this.setState({ prompt: false }); + if (k) { + this.refs.tab.edit(k); + } + }).bind(this), + options: options + } + }); + }, + render: function render() { + var flow = this.props.flow; + var tabs = this.getTabs(flow); + var active = this.getActive(); + + if (!_.contains(tabs, active)) { + if (active === "response" && flow.error) { + active = "error"; + } else if (active === "error" && flow.response) { + active = "response"; + } else { + active = tabs[0]; + } + this.selectTab(active); + } + + var prompt = null; + if (this.state.prompt) { + prompt = React.createElement(Prompt, this.state.prompt); + } + + var Tab = allTabs[active]; + return React.createElement( + "div", + { className: "flow-detail", onScroll: this.adjustHead }, + React.createElement(Nav, { ref: "head", + flow: flow, + tabs: tabs, + active: active, + selectTab: this.selectTab }), + React.createElement(Tab, { ref: "tab", flow: flow }), + prompt + ); + } +}); + +module.exports = FlowView; + + +},{"../common.js":4,"../prompt.js":17,"./details.js":10,"./messages.js":12,"./nav.js":13,"lodash":"lodash","react":"react"}],12:[function(require,module,exports){ +"use strict"; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var actions = require("../../actions.js"); +var flowutils = require("../../flow/utils.js"); +var utils = require("../../utils.js"); +var ContentView = require("./contentview.js"); +var ValueEditor = require("../editor.js").ValueEditor; + +var Headers = React.createClass({ + displayName: "Headers", + + propTypes: { + onChange: React.PropTypes.func.isRequired, + message: React.PropTypes.object.isRequired + }, + onChange: function onChange(row, col, val) { + var nextHeaders = _.cloneDeep(this.props.message.headers); + nextHeaders[row][col] = val; + if (!nextHeaders[row][0] && !nextHeaders[row][1]) { + // do not delete last row + if (nextHeaders.length === 1) { + nextHeaders[0][0] = "Name"; + nextHeaders[0][1] = "Value"; + } else { + nextHeaders.splice(row, 1); + // manually move selection target if this has been the last row. + if (row === nextHeaders.length) { + this._nextSel = row - 1 + "-value"; + } + } + } + this.props.onChange(nextHeaders); + }, + edit: function edit() { + this.refs["0-key"].focus(); + }, + onTab: function onTab(row, col, e) { + var headers = this.props.message.headers; + if (row === headers.length - 1 && col === 1) { + e.preventDefault(); + + var nextHeaders = _.cloneDeep(this.props.message.headers); + nextHeaders.push(["Name", "Value"]); + this.props.onChange(nextHeaders); + this._nextSel = row + 1 + "-key"; + } + }, + componentDidUpdate: function componentDidUpdate() { + if (this._nextSel && this.refs[this._nextSel]) { + this.refs[this._nextSel].focus(); + this._nextSel = undefined; + } + }, + onRemove: function onRemove(row, col, e) { + if (col === 1) { + e.preventDefault(); + this.refs[row + "-key"].focus(); + } else if (row > 0) { + e.preventDefault(); + this.refs[row - 1 + "-value"].focus(); + } + }, + render: function render() { + + var rows = this.props.message.headers.map((function (header, i) { + + var kEdit = React.createElement(HeaderEditor, { + ref: i + "-key", + content: header[0], + onDone: this.onChange.bind(null, i, 0), + onRemove: this.onRemove.bind(null, i, 0), + onTab: this.onTab.bind(null, i, 0) }); + var vEdit = React.createElement(HeaderEditor, { + ref: i + "-value", + content: header[1], + onDone: this.onChange.bind(null, i, 1), + onRemove: this.onRemove.bind(null, i, 1), + onTab: this.onTab.bind(null, i, 1) }); + return React.createElement( + "tr", + { key: i }, + React.createElement( + "td", + { className: "header-name" }, + kEdit, + ":" + ), + React.createElement( + "td", + { className: "header-value" }, + vEdit + ) + ); + }).bind(this)); + return React.createElement( + "table", + { className: "header-table" }, + React.createElement( + "tbody", + null, + rows + ) + ); + } +}); + +var HeaderEditor = React.createClass({ + displayName: "HeaderEditor", + + render: function render() { + return React.createElement(ValueEditor, _extends({ ref: "input" }, this.props, { onKeyDown: this.onKeyDown, inline: true })); + }, + focus: function focus() { + this.getDOMNode().focus(); + }, + onKeyDown: function onKeyDown(e) { + switch (e.keyCode) { + case utils.Key.BACKSPACE: + var s = window.getSelection().getRangeAt(0); + if (s.startOffset === 0 && s.endOffset === 0) { + this.props.onRemove(e); + } + break; + case utils.Key.TAB: + if (!e.shiftKey) { + this.props.onTab(e); + } + break; + } + } +}); + +var RequestLine = React.createClass({ + displayName: "RequestLine", + + render: function render() { + var flow = this.props.flow; + var url = flowutils.RequestUtils.pretty_url(flow.request); + var httpver = flow.request.http_version; + + return React.createElement( + "div", + { className: "first-line request-line" }, + React.createElement(ValueEditor, { + ref: "method", + content: flow.request.method, + onDone: this.onMethodChange, + inline: true }), + " ", + React.createElement(ValueEditor, { + ref: "url", + content: url, + onDone: this.onUrlChange, + isValid: this.isValidUrl, + inline: true }), + " ", + React.createElement(ValueEditor, { + ref: "httpVersion", + content: httpver, + onDone: this.onHttpVersionChange, + isValid: flowutils.isValidHttpVersion, + inline: true }) + ); + }, + isValidUrl: function isValidUrl(url) { + var u = flowutils.parseUrl(url); + return !!u.host; + }, + onMethodChange: function onMethodChange(nextMethod) { + actions.FlowActions.update(this.props.flow, { request: { method: nextMethod } }); + }, + onUrlChange: function onUrlChange(nextUrl) { + var props = flowutils.parseUrl(nextUrl); + props.path = props.path || ""; + actions.FlowActions.update(this.props.flow, { request: props }); + }, + onHttpVersionChange: function onHttpVersionChange(nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update(this.props.flow, { request: { http_version: ver } }); + } +}); + +var ResponseLine = React.createClass({ + displayName: "ResponseLine", + + render: function render() { + var flow = this.props.flow; + var httpver = flow.response.http_version; + return React.createElement( + "div", + { className: "first-line response-line" }, + React.createElement(ValueEditor, { + ref: "httpVersion", + content: httpver, + onDone: this.onHttpVersionChange, + isValid: flowutils.isValidHttpVersion, + inline: true }), + " ", + React.createElement(ValueEditor, { + ref: "code", + content: flow.response.status_code + "", + onDone: this.onCodeChange, + isValid: this.isValidCode, + inline: true }), + " ", + React.createElement(ValueEditor, { + ref: "msg", + content: flow.response.msg, + onDone: this.onMsgChange, + inline: true }) + ); + }, + isValidCode: function isValidCode(code) { + return (/^\d+$/.test(code) + ); + }, + onHttpVersionChange: function onHttpVersionChange(nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update(this.props.flow, { response: { http_version: ver } }); + }, + onMsgChange: function onMsgChange(nextMsg) { + actions.FlowActions.update(this.props.flow, { response: { msg: nextMsg } }); + }, + onCodeChange: function onCodeChange(nextCode) { + nextCode = parseInt(nextCode); + actions.FlowActions.update(this.props.flow, { response: { code: nextCode } }); + } +}); + +var Request = React.createClass({ + displayName: "Request", + + render: function render() { + var flow = this.props.flow; + return React.createElement( + "section", + { className: "request" }, + React.createElement(RequestLine, { ref: "requestLine", flow: flow }), + React.createElement(Headers, { ref: "headers", message: flow.request, onChange: this.onHeaderChange }), + React.createElement("hr", null), + React.createElement(ContentView, { flow: flow, message: flow.request }) + ); + }, + edit: function edit(k) { + switch (k) { + case "m": + this.refs.requestLine.refs.method.focus(); + break; + case "u": + this.refs.requestLine.refs.url.focus(); + break; + case "v": + this.refs.requestLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: " + k; + } + }, + onHeaderChange: function onHeaderChange(nextHeaders) { + actions.FlowActions.update(this.props.flow, { + request: { + headers: nextHeaders + } + }); + } +}); + +var Response = React.createClass({ + displayName: "Response", + + render: function render() { + var flow = this.props.flow; + return React.createElement( + "section", + { className: "response" }, + React.createElement(ResponseLine, { ref: "responseLine", flow: flow }), + React.createElement(Headers, { ref: "headers", message: flow.response, onChange: this.onHeaderChange }), + React.createElement("hr", null), + React.createElement(ContentView, { flow: flow, message: flow.response }) + ); + }, + edit: function edit(k) { + switch (k) { + case "c": + this.refs.responseLine.refs.status_code.focus(); + break; + case "m": + this.refs.responseLine.refs.msg.focus(); + break; + case "v": + this.refs.responseLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: " + k; + } + }, + onHeaderChange: function onHeaderChange(nextHeaders) { + actions.FlowActions.update(this.props.flow, { + response: { + headers: nextHeaders + } + }); + } +}); + +var Error = React.createClass({ + displayName: "Error", + + render: function render() { + var flow = this.props.flow; + return React.createElement( + "section", + null, + React.createElement( + "div", + { className: "alert alert-warning" }, + flow.error.msg, + React.createElement( + "div", + null, + React.createElement( + "small", + null, + utils.formatTimeStamp(flow.error.timestamp) + ) + ) + ) + ); + } +}); + +module.exports = { + Request: Request, + Response: Response, + Error: Error +}; +/**/ /**/ + + +},{"../../actions.js":2,"../../flow/utils.js":23,"../../utils.js":26,"../common.js":4,"../editor.js":5,"./contentview.js":9,"lodash":"lodash","react":"react"}],13:[function(require,module,exports){ +"use strict"; + +var React = require("react"); + +var actions = require("../../actions.js"); + +var NavAction = React.createClass({ + displayName: "NavAction", + + onClick: function onClick(e) { + e.preventDefault(); + this.props.onClick(); + }, + render: function render() { + return React.createElement( + "a", + { title: this.props.title, + href: "#", + className: "nav-action", + onClick: this.onClick }, + React.createElement("i", { className: "fa fa-fw " + this.props.icon }) + ); + } +}); + +var Nav = React.createClass({ + displayName: "Nav", + + render: function render() { + var flow = this.props.flow; + + var tabs = this.props.tabs.map((function (e) { + var str = e.charAt(0).toUpperCase() + e.slice(1); + var className = this.props.active === e ? "active" : ""; + var onClick = (function (event) { + this.props.selectTab(e); + event.preventDefault(); + }).bind(this); + return React.createElement( + "a", + { key: e, + href: "#", + className: className, + onClick: onClick }, + str + ); + }).bind(this)); + + var acceptButton = null; + if (flow.intercepted) { + acceptButton = React.createElement(NavAction, { title: "[a]ccept intercepted flow", icon: "fa-play", onClick: actions.FlowActions.accept.bind(null, flow) }); + } + var revertButton = null; + if (flow.modified) { + revertButton = React.createElement(NavAction, { title: "revert changes to flow [V]", icon: "fa-history", onClick: actions.FlowActions.revert.bind(null, flow) }); + } + + return React.createElement( + "nav", + { ref: "head", className: "nav-tabs nav-tabs-sm" }, + tabs, + React.createElement(NavAction, { title: "[d]elete flow", icon: "fa-trash", onClick: actions.FlowActions["delete"].bind(null, flow) }), + React.createElement(NavAction, { title: "[D]uplicate flow", icon: "fa-copy", onClick: actions.FlowActions.duplicate.bind(null, flow) }), + React.createElement(NavAction, { disabled: true, title: "[r]eplay flow", icon: "fa-repeat", onClick: actions.FlowActions.replay.bind(null, flow) }), + acceptButton, + revertButton + ); + } +}); + +module.exports = Nav; + + +},{"../../actions.js":2,"react":"react"}],14:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var common = require("./common.js"); + +var Footer = React.createClass({ + displayName: "Footer", + + mixins: [common.SettingsState], + render: function render() { + var mode = this.state.settings.mode; + var intercept = this.state.settings.intercept; + return React.createElement( + "footer", + null, + mode && mode != "regular" ? React.createElement( + "span", + { className: "label label-success" }, + mode, + " mode" + ) : null, + " ", + intercept ? React.createElement( + "span", + { className: "label label-success" }, + "Intercept: ", + intercept + ) : null + ); + } +}); + +module.exports = Footer; + + +},{"./common.js":4,"react":"react"}],15:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var $ = require("jquery"); + +var Filt = require("../filt/filt.js"); +var utils = require("../utils.js"); +var common = require("./common.js"); +var actions = require("../actions.js"); +var Query = require("../actions.js").Query; + +var FilterDocs = React.createClass({ + displayName: "FilterDocs", + + statics: { + xhr: false, + doc: false + }, + componentWillMount: function componentWillMount() { + if (!FilterDocs.doc) { + FilterDocs.xhr = $.getJSON("/filter-help").done(function (doc) { + FilterDocs.doc = doc; + FilterDocs.xhr = false; + }); + } + if (FilterDocs.xhr) { + FilterDocs.xhr.done((function () { + this.forceUpdate(); + }).bind(this)); + } + }, + render: function render() { + if (!FilterDocs.doc) { + return React.createElement("i", { className: "fa fa-spinner fa-spin" }); + } else { + var commands = FilterDocs.doc.commands.map(function (c) { + return React.createElement( + "tr", + { key: c[1] }, + React.createElement( + "td", + null, + c[0].replace(" ", " ") + ), + React.createElement( + "td", + null, + c[1] + ) + ); + }); + commands.push(React.createElement( + "tr", + { key: "docs-link" }, + React.createElement( + "td", + { colSpan: "2" }, + React.createElement( + "a", + { href: "https://mitmproxy.org/doc/features/filters.html", + target: "_blank" }, + React.createElement("i", { className: "fa fa-external-link" }), + "  mitmproxy docs" + ) + ) + )); + return React.createElement( + "table", + { className: "table table-condensed" }, + React.createElement( + "tbody", + null, + commands + ) + ); + } + } +}); +var FilterInput = React.createClass({ + displayName: "FilterInput", + + mixins: [common.ChildFocus], + getInitialState: function getInitialState() { + // Consider both focus and mouseover for showing/hiding the tooltip, + // because onBlur of the input is triggered before the click on the tooltip + // finalized, hiding the tooltip just as the user clicks on it. + return { + value: this.props.value, + focus: false, + mousefocus: false + }; + }, + componentWillReceiveProps: function componentWillReceiveProps(nextProps) { + this.setState({ value: nextProps.value }); + }, + onChange: function onChange(e) { + var nextValue = e.target.value; + this.setState({ + value: nextValue + }); + // Only propagate valid filters upwards. + if (this.isValid(nextValue)) { + this.props.onChange(nextValue); + } + }, + isValid: function isValid(filt) { + try { + Filt.parse(filt || this.state.value); + return true; + } catch (e) { + return false; + } + }, + getDesc: function getDesc() { + var desc; + try { + desc = Filt.parse(this.state.value).desc; + } catch (e) { + desc = "" + e; + } + if (desc !== "true") { + return desc; + } else { + return React.createElement(FilterDocs, null); + } + }, + onFocus: function onFocus() { + this.setState({ focus: true }); + }, + onBlur: function onBlur() { + this.setState({ focus: false }); + }, + onMouseEnter: function onMouseEnter() { + this.setState({ mousefocus: true }); + }, + onMouseLeave: function onMouseLeave() { + this.setState({ mousefocus: false }); + }, + onKeyDown: function onKeyDown(e) { + if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.blur(); + // If closed using ESC/ENTER, hide the tooltip. + this.setState({ mousefocus: false }); + } + e.stopPropagation(); + }, + blur: function blur() { + this.refs.input.getDOMNode().blur(); + this.returnFocus(); + }, + select: function select() { + this.refs.input.getDOMNode().select(); + }, + render: function render() { + var isValid = this.isValid(); + var icon = "fa fa-fw fa-" + this.props.type; + var groupClassName = "filter-input input-group" + (isValid ? "" : " has-error"); + + var popover; + if (this.state.focus || this.state.mousefocus) { + popover = React.createElement( + "div", + { className: "popover bottom", onMouseEnter: this.onMouseEnter, onMouseLeave: this.onMouseLeave }, + React.createElement("div", { className: "arrow" }), + React.createElement( + "div", + { className: "popover-content" }, + this.getDesc() + ) + ); + } + + return React.createElement( + "div", + { className: groupClassName }, + React.createElement( + "span", + { className: "input-group-addon" }, + React.createElement("i", { className: icon, style: { color: this.props.color } }) + ), + React.createElement("input", { type: "text", placeholder: this.props.placeholder, className: "form-control", + ref: "input", + onChange: this.onChange, + onFocus: this.onFocus, + onBlur: this.onBlur, + onKeyDown: this.onKeyDown, + value: this.state.value }), + popover + ); + } +}); + +var MainMenu = React.createClass({ + displayName: "MainMenu", + + mixins: [common.Navigation, common.RouterState, common.SettingsState], + statics: { + title: "Start", + route: "flows" + }, + onSearchChange: function onSearchChange(val) { + var d = {}; + d[Query.SEARCH] = val; + this.setQuery(d); + }, + onHighlightChange: function onHighlightChange(val) { + var d = {}; + d[Query.HIGHLIGHT] = val; + this.setQuery(d); + }, + onInterceptChange: function onInterceptChange(val) { + actions.SettingsActions.update({ intercept: val }); + }, + render: function render() { + var search = this.getQuery()[Query.SEARCH] || ""; + var highlight = this.getQuery()[Query.HIGHLIGHT] || ""; + var intercept = this.state.settings.intercept || ""; + + return React.createElement( + "div", + null, + React.createElement( + "div", + { className: "menu-row" }, + React.createElement(FilterInput, { + ref: "search", + placeholder: "Search", + type: "search", + color: "black", + value: search, + onChange: this.onSearchChange }), + React.createElement(FilterInput, { + ref: "highlight", + placeholder: "Highlight", + type: "tag", + color: "hsl(48, 100%, 50%)", + value: highlight, + onChange: this.onHighlightChange }), + React.createElement(FilterInput, { + ref: "intercept", + placeholder: "Intercept", + type: "pause", + color: "hsl(208, 56%, 53%)", + value: intercept, + onChange: this.onInterceptChange }) + ), + React.createElement("div", { className: "clearfix" }) + ); + } +}); + +var ViewMenu = React.createClass({ + displayName: "ViewMenu", + + statics: { + title: "View", + route: "flows" + }, + mixins: [common.Navigation, common.RouterState], + toggleEventLog: function toggleEventLog() { + var d = {}; + + if (this.getQuery()[Query.SHOW_EVENTLOG]) { + d[Query.SHOW_EVENTLOG] = undefined; + } else { + d[Query.SHOW_EVENTLOG] = "t"; // any non-false value will do it, keep it short + } + + this.setQuery(d); + }, + render: function render() { + var showEventLog = this.getQuery()[Query.SHOW_EVENTLOG]; + return React.createElement( + "div", + null, + React.createElement( + "button", + { + className: "btn " + (showEventLog ? "btn-primary" : "btn-default"), + onClick: this.toggleEventLog }, + React.createElement("i", { className: "fa fa-database" }), + " Show Eventlog" + ), + React.createElement( + "span", + null, + " " + ) + ); + } +}); + +var ReportsMenu = React.createClass({ + displayName: "ReportsMenu", + + statics: { + title: "Visualization", + route: "reports" + }, + render: function render() { + return React.createElement( + "div", + null, + "Reports Menu" + ); + } +}); + +var FileMenu = React.createClass({ + displayName: "FileMenu", + + getInitialState: function getInitialState() { + return { + showFileMenu: false + }; + }, + handleFileClick: function handleFileClick(e) { + e.preventDefault(); + if (!this.state.showFileMenu) { + var close = (function () { + this.setState({ showFileMenu: false }); + document.removeEventListener("click", close); + }).bind(this); + document.addEventListener("click", close); + + this.setState({ + showFileMenu: true + }); + } + }, + handleNewClick: function handleNewClick(e) { + e.preventDefault(); + if (confirm("Delete all flows?")) { + actions.FlowActions.clear(); + } + }, + handleOpenClick: function handleOpenClick(e) { + e.preventDefault(); + console.error("unimplemented: handleOpenClick"); + }, + handleSaveClick: function handleSaveClick(e) { + e.preventDefault(); + console.error("unimplemented: handleSaveClick"); + }, + handleShutdownClick: function handleShutdownClick(e) { + e.preventDefault(); + console.error("unimplemented: handleShutdownClick"); + }, + render: function render() { + var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : ""); + + return React.createElement( + "div", + { className: fileMenuClass }, + React.createElement( + "a", + { href: "#", className: "special", onClick: this.handleFileClick }, + " mitmproxy " + ), + React.createElement( + "ul", + { className: "dropdown-menu", role: "menu" }, + React.createElement( + "li", + null, + React.createElement( + "a", + { href: "#", onClick: this.handleNewClick }, + React.createElement("i", { className: "fa fa-fw fa-file" }), + "New" + ) + ), + React.createElement("li", { role: "presentation", className: "divider" }), + React.createElement( + "li", + null, + React.createElement( + "a", + { href: "http://mitm.it/", target: "_blank" }, + React.createElement("i", { className: "fa fa-fw fa-external-link" }), + "Install Certificates..." + ) + ) + ) + ); + } +}); + +var header_entries = [MainMenu, ViewMenu /*, ReportsMenu */]; + +var Header = React.createClass({ + displayName: "Header", + + mixins: [common.Navigation], + getInitialState: function getInitialState() { + return { + active: header_entries[0] + }; + }, + handleClick: function handleClick(active, e) { + e.preventDefault(); + this.replaceWith(active.route); + this.setState({ active: active }); + }, + render: function render() { + var header = header_entries.map((function (entry, i) { + var className; + if (entry === this.state.active) { + className = "active"; + } else { + className = ""; + } + return React.createElement( + "a", + { key: i, + href: "#", + className: className, + onClick: this.handleClick.bind(this, entry) }, + entry.title + ); + }).bind(this)); + + return React.createElement( + "header", + null, + React.createElement( + "nav", + { className: "nav-tabs nav-tabs-lg" }, + React.createElement(FileMenu, null), + header + ), + React.createElement( + "div", + { className: "menu" }, + React.createElement(this.state.active, { ref: "active" }) + ) + ); + } +}); + +module.exports = { + Header: Header, + MainMenu: MainMenu +}; +/* +
  • + + +Open + +
  • +
  • + + +Save + +
  • +
  • +
  • + + +Shutdown + +
  • +*/ + + +},{"../actions.js":2,"../filt/filt.js":22,"../utils.js":26,"./common.js":4,"jquery":"jquery","react":"react"}],16:[function(require,module,exports){ +"use strict"; + +var React = require("react"); + +var actions = require("../actions.js"); +var Query = require("../actions.js").Query; +var utils = require("../utils.js"); +var views = require("../store/view.js"); +var Filt = require("../filt/filt.js"); + +var common = require("./common.js"); +var FlowTable = require("./flowtable.js"); +var FlowView = require("./flowview/index.js"); + +var MainView = React.createClass({ + displayName: "MainView", + + mixins: [common.Navigation, common.RouterState], + contextTypes: { + flowStore: React.PropTypes.object.isRequired + }, + childContextTypes: { + view: React.PropTypes.object.isRequired + }, + getChildContext: function getChildContext() { + return { + view: this.state.view + }; + }, + getInitialState: function getInitialState() { + var sortKeyFun = false; + var view = new views.StoreView(this.context.flowStore, this.getViewFilt(), sortKeyFun); + view.addListener("recalculate", this.onRecalculate); + view.addListener("add", this.onUpdate); + view.addListener("update", this.onUpdate); + view.addListener("remove", this.onUpdate); + view.addListener("remove", this.onRemove); + + return { + view: view, + sortKeyFun: sortKeyFun + }; + }, + componentWillUnmount: function componentWillUnmount() { + this.state.view.close(); + }, + getViewFilt: function getViewFilt() { + try { + var filt = Filt.parse(this.getQuery()[Query.SEARCH] || ""); + var highlightStr = this.getQuery()[Query.HIGHLIGHT]; + var highlight = highlightStr ? Filt.parse(highlightStr) : false; + } catch (e) { + console.error("Error when processing filter: " + e); + } + + return function filter_and_highlight(flow) { + if (!this._highlight) { + this._highlight = {}; + } + this._highlight[flow.id] = highlight && highlight(flow); + return filt(flow); + }; + }, + componentWillReceiveProps: function componentWillReceiveProps(nextProps) { + var filterChanged = this.props.query[Query.SEARCH] !== nextProps.query[Query.SEARCH]; + var highlightChanged = this.props.query[Query.HIGHLIGHT] !== nextProps.query[Query.HIGHLIGHT]; + if (filterChanged || highlightChanged) { + this.state.view.recalculate(this.getViewFilt(), this.state.sortKeyFun); + } + }, + onRecalculate: function onRecalculate() { + this.forceUpdate(); + var selected = this.getSelected(); + if (selected) { + this.refs.flowTable.scrollIntoView(selected); + } + }, + onUpdate: function onUpdate(flow) { + if (flow.id === this.getParams().flowId) { + this.forceUpdate(); + } + }, + onRemove: function onRemove(flow_id, index) { + if (flow_id === this.getParams().flowId) { + var flow_to_select = this.state.view.list[Math.min(index, this.state.view.list.length - 1)]; + this.selectFlow(flow_to_select); + } + }, + setSortKeyFun: function setSortKeyFun(sortKeyFun) { + this.setState({ + sortKeyFun: sortKeyFun + }); + this.state.view.recalculate(this.getViewFilt(), sortKeyFun); + }, + selectFlow: function selectFlow(flow) { + if (flow) { + this.replaceWith("flow", { + flowId: flow.id, + detailTab: this.getParams().detailTab || "request" + }); + this.refs.flowTable.scrollIntoView(flow); + } else { + this.replaceWith("flows", {}); + } + }, + selectFlowRelative: function selectFlowRelative(shift) { + var flows = this.state.view.list; + var index; + if (!this.getParams().flowId) { + if (shift < 0) { + index = flows.length - 1; + } else { + index = 0; + } + } else { + var currFlowId = this.getParams().flowId; + var i = flows.length; + while (i--) { + if (flows[i].id === currFlowId) { + index = i; + break; + } + } + index = Math.min(Math.max(0, index + shift), flows.length - 1); + } + this.selectFlow(flows[index]); + }, + onMainKeyDown: function onMainKeyDown(e) { + var flow = this.getSelected(); + if (e.ctrlKey) { + return; + } + switch (e.keyCode) { + case utils.Key.K: + case utils.Key.UP: + this.selectFlowRelative(-1); + break; + case utils.Key.J: + case utils.Key.DOWN: + this.selectFlowRelative(+1); + break; + case utils.Key.SPACE: + case utils.Key.PAGE_DOWN: + this.selectFlowRelative(+10); + break; + case utils.Key.PAGE_UP: + this.selectFlowRelative(-10); + break; + case utils.Key.END: + this.selectFlowRelative(+1e10); + break; + case utils.Key.HOME: + this.selectFlowRelative(-1e10); + break; + case utils.Key.ESC: + this.selectFlow(null); + break; + case utils.Key.H: + case utils.Key.LEFT: + if (this.refs.flowDetails) { + this.refs.flowDetails.nextTab(-1); + } + break; + case utils.Key.L: + case utils.Key.TAB: + case utils.Key.RIGHT: + if (this.refs.flowDetails) { + this.refs.flowDetails.nextTab(+1); + } + break; + case utils.Key.C: + if (e.shiftKey) { + actions.FlowActions.clear(); + } + break; + case utils.Key.D: + if (flow) { + if (e.shiftKey) { + actions.FlowActions.duplicate(flow); + } else { + actions.FlowActions["delete"](flow); + } + } + break; + case utils.Key.A: + if (e.shiftKey) { + actions.FlowActions.accept_all(); + } else if (flow && flow.intercepted) { + actions.FlowActions.accept(flow); + } + break; + case utils.Key.R: + if (!e.shiftKey && flow) { + actions.FlowActions.replay(flow); + } + break; + case utils.Key.V: + if (e.shiftKey && flow && flow.modified) { + actions.FlowActions.revert(flow); + } + break; + case utils.Key.E: + if (this.refs.flowDetails) { + this.refs.flowDetails.promptEdit(); + } + break; + case utils.Key.SHIFT: + break; + default: + console.debug("keydown", e.keyCode); + return; + } + e.preventDefault(); + }, + getSelected: function getSelected() { + return this.context.flowStore.get(this.getParams().flowId); + }, + render: function render() { + var selected = this.getSelected(); + + var details; + if (selected) { + details = [React.createElement(common.Splitter, { key: "splitter" }), React.createElement(FlowView, { key: "flowDetails", ref: "flowDetails", flow: selected })]; + } else { + details = null; + } + + return React.createElement( + "div", + { className: "main-view" }, + React.createElement(FlowTable, { ref: "flowTable", + selectFlow: this.selectFlow, + setSortKeyFun: this.setSortKeyFun, + selected: selected }), + details + ); + } +}); + +module.exports = MainView; + + +},{"../actions.js":2,"../filt/filt.js":22,"../store/view.js":25,"../utils.js":26,"./common.js":4,"./flowtable.js":8,"./flowview/index.js":11,"react":"react"}],17:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../utils.js"); +var common = require("./common.js"); + +var Prompt = React.createClass({ + displayName: "Prompt", + + mixins: [common.ChildFocus], + propTypes: { + options: React.PropTypes.array.isRequired, + done: React.PropTypes.func.isRequired, + prompt: React.PropTypes.string + }, + componentDidMount: function componentDidMount() { + React.findDOMNode(this).focus(); + }, + onKeyDown: function onKeyDown(e) { + e.stopPropagation(); + e.preventDefault(); + var opts = this.getOptions(); + for (var i = 0; i < opts.length; i++) { + var k = opts[i].key; + if (utils.Key[k.toUpperCase()] === e.keyCode) { + this.done(k); + return; + } + } + if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.done(false); + } + }, + onClick: function onClick(e) { + this.done(false); + }, + done: function done(ret) { + this.props.done(ret); + this.returnFocus(); + }, + getOptions: function getOptions() { + var opts = []; + + var keyTaken = function keyTaken(k) { + return _.includes(_.pluck(opts, "key"), k); + }; + + for (var i = 0; i < this.props.options.length; i++) { + var opt = this.props.options[i]; + if (_.isString(opt)) { + var str = opt; + while (str.length > 0 && keyTaken(str[0])) { + str = str.substr(1); + } + opt = { + text: opt, + key: str[0] + }; + } + if (!opt.text || !opt.key || keyTaken(opt.key)) { + throw "invalid options"; + } else { + opts.push(opt); + } + } + return opts; + }, + render: function render() { + var opts = this.getOptions(); + opts = _.map(opts, (function (o) { + var prefix, suffix; + var idx = o.text.indexOf(o.key); + if (idx !== -1) { + prefix = o.text.substring(0, idx); + suffix = o.text.substring(idx + 1); + } else { + prefix = o.text + " ("; + suffix = ")"; + } + var onClick = (function (e) { + this.done(o.key); + e.stopPropagation(); + }).bind(this); + return React.createElement( + "span", + { + key: o.key, + className: "option", + onClick: onClick }, + prefix, + React.createElement( + "strong", + { className: "text-primary" }, + o.key + ), + suffix + ); + }).bind(this)); + return React.createElement( + "div", + { tabIndex: "0", onKeyDown: this.onKeyDown, onClick: this.onClick, className: "prompt-dialog" }, + React.createElement( + "div", + { className: "prompt-content" }, + this.props.prompt || React.createElement( + "strong", + null, + "Select: " + ), + opts + ) + ); + } +}); + +module.exports = Prompt; + + +},{"../utils.js":26,"./common.js":4,"lodash":"lodash","react":"react"}],18:[function(require,module,exports){ +"use strict"; + +var React = require("react"); +var ReactRouter = require("react-router"); +var _ = require("lodash"); + +var common = require("./common.js"); +var MainView = require("./mainview.js"); +var Footer = require("./footer.js"); +var header = require("./header.js"); +var EventLog = require("./eventlog.js"); +var store = require("../store/store.js"); +var Query = require("../actions.js").Query; +var Key = require("../utils.js").Key; + +//TODO: Move out of here, just a stub. +var Reports = React.createClass({ + displayName: "Reports", + + render: function render() { + return React.createElement( + "div", + null, + "ReportEditor" + ); + } +}); + +var ProxyAppMain = React.createClass({ + displayName: "ProxyAppMain", + + mixins: [common.RouterState], + childContextTypes: { + settingsStore: React.PropTypes.object.isRequired, + flowStore: React.PropTypes.object.isRequired, + eventStore: React.PropTypes.object.isRequired, + returnFocus: React.PropTypes.func.isRequired + }, + componentDidMount: function componentDidMount() { + this.focus(); + }, + getChildContext: function getChildContext() { + return { + settingsStore: this.state.settingsStore, + flowStore: this.state.flowStore, + eventStore: this.state.eventStore, + returnFocus: this.focus + }; + }, + getInitialState: function getInitialState() { + var eventStore = new store.EventLogStore(); + var flowStore = new store.FlowStore(); + var settingsStore = new store.SettingsStore(); + + // Default Settings before fetch + _.extend(settingsStore.dict, {}); + return { + settingsStore: settingsStore, + flowStore: flowStore, + eventStore: eventStore + }; + }, + focus: function focus() { + React.findDOMNode(this).focus(); + }, + getMainComponent: function getMainComponent() { + return this.refs.view.refs.__routeHandler__; + }, + onKeydown: function onKeydown(e) { + + var selectFilterInput = (function (name) { + var headerComponent = this.refs.header; + headerComponent.setState({ active: header.MainMenu }, function () { + headerComponent.refs.active.refs[name].select(); + }); + }).bind(this); + + switch (e.keyCode) { + case Key.I: + selectFilterInput("intercept"); + break; + case Key.L: + selectFilterInput("search"); + break; + case Key.H: + selectFilterInput("highlight"); + break; + default: + var main = this.getMainComponent(); + if (main.onMainKeyDown) { + main.onMainKeyDown(e); + } + return; // don't prevent default then + } + e.preventDefault(); + }, + render: function render() { + var eventlog; + if (this.getQuery()[Query.SHOW_EVENTLOG]) { + eventlog = [React.createElement(common.Splitter, { key: "splitter", axis: "y" }), React.createElement(EventLog, { key: "eventlog" })]; + } else { + eventlog = null; + } + return React.createElement( + "div", + { id: "container", tabIndex: "0", onKeyDown: this.onKeydown }, + React.createElement(header.Header, { ref: "header" }), + React.createElement(RouteHandler, { ref: "view", query: this.getQuery() }), + eventlog, + React.createElement(Footer, null) + ); + } +}); + +var Route = ReactRouter.Route; +var RouteHandler = ReactRouter.RouteHandler; +var Redirect = ReactRouter.Redirect; +var DefaultRoute = ReactRouter.DefaultRoute; +var NotFoundRoute = ReactRouter.NotFoundRoute; + +var routes = React.createElement( + Route, + { path: "/", handler: ProxyAppMain }, + React.createElement(Route, { name: "flows", path: "flows", handler: MainView }), + React.createElement(Route, { name: "flow", path: "flows/:flowId/:detailTab", handler: MainView }), + React.createElement(Route, { name: "reports", handler: Reports }), + React.createElement(Redirect, { path: "/", to: "flows" }) +); + +module.exports = { + routes: routes +}; + + +},{"../actions.js":2,"../store/store.js":24,"../utils.js":26,"./common.js":4,"./eventlog.js":6,"./footer.js":14,"./header.js":15,"./mainview.js":16,"lodash":"lodash","react":"react","react-router":"react-router"}],19:[function(require,module,exports){ +"use strict"; + +var React = require("react"); + +var VirtualScrollMixin = { + getInitialState: function getInitialState() { + return { + start: 0, + stop: 0 + }; + }, + componentWillMount: function componentWillMount() { + if (!this.props.rowHeight) { + console.warn("VirtualScrollMixin: No rowHeight specified", this); + } + }, + getPlaceholderTop: function getPlaceholderTop(total) { + var Tag = this.props.placeholderTagName || "tr"; + // When a large trunk of elements is removed from the button, start may be far off the viewport. + // To make this issue less severe, limit the top placeholder to the total number of rows. + var style = { + height: Math.min(this.state.start, total) * this.props.rowHeight + }; + var spacer = React.createElement(Tag, { key: "placeholder-top", style: style }); + + if (this.state.start % 2 === 1) { + // fix even/odd rows + return [spacer, React.createElement(Tag, { key: "placeholder-top-2" })]; + } else { + return spacer; + } + }, + getPlaceholderBottom: function getPlaceholderBottom(total) { + var Tag = this.props.placeholderTagName || "tr"; + var style = { + height: Math.max(0, total - this.state.stop) * this.props.rowHeight + }; + return React.createElement(Tag, { key: "placeholder-bottom", style: style }); + }, + componentDidMount: function componentDidMount() { + this.onScroll(); + window.addEventListener('resize', this.onScroll); + }, + componentWillUnmount: function componentWillUnmount() { + window.removeEventListener('resize', this.onScroll); + }, + onScroll: function onScroll() { + var viewport = this.getDOMNode(); + var top = viewport.scrollTop; + var height = viewport.offsetHeight; + var start = Math.floor(top / this.props.rowHeight); + var stop = start + Math.ceil(height / (this.props.rowHeightMin || this.props.rowHeight)); + + this.setState({ + start: start, + stop: stop + }); + }, + renderRows: function renderRows(elems) { + var rows = []; + var max = Math.min(elems.length, this.state.stop); + + for (var i = this.state.start; i < max; i++) { + var elem = elems[i]; + rows.push(this.renderRow(elem)); + } + return rows; + }, + scrollRowIntoView: function scrollRowIntoView(index, head_height) { + + var row_top = index * this.props.rowHeight + head_height; + var row_bottom = row_top + this.props.rowHeight; + + var viewport = this.getDOMNode(); + var viewport_top = viewport.scrollTop; + var viewport_bottom = viewport_top + viewport.offsetHeight; + + // Account for pinned thead + if (row_top - head_height < viewport_top) { + viewport.scrollTop = row_top - head_height; + } else if (row_bottom > viewport_bottom) { + viewport.scrollTop = row_bottom - viewport.offsetHeight; + } + } +}; + +module.exports = VirtualScrollMixin; + + +},{"react":"react"}],20:[function(require,module,exports){ +"use strict"; + +var actions = require("./actions.js"); +var AppDispatcher = require("./dispatcher.js").AppDispatcher; + +function Connection(url) { + if (url[0] === "/") { + url = location.origin.replace("http", "ws") + url; + } + + var ws = new WebSocket(url); + ws.onopen = function () { + actions.ConnectionActions.open(); + }; + ws.onmessage = function (message) { + var m = JSON.parse(message.data); + AppDispatcher.dispatchServerAction(m); + }; + ws.onerror = function () { + actions.ConnectionActions.error(); + actions.EventLogActions.add_event("WebSocket connection error."); + }; + ws.onclose = function () { + actions.ConnectionActions.close(); + actions.EventLogActions.add_event("WebSocket connection closed."); + }; + return ws; +} + +module.exports = Connection; + + +},{"./actions.js":2,"./dispatcher.js":21}],21:[function(require,module,exports){ +"use strict"; + +var flux = require("flux"); + +var PayloadSources = { + VIEW: "view", + SERVER: "server" +}; + +var AppDispatcher = new flux.Dispatcher(); +AppDispatcher.dispatchViewAction = function (action) { + action.source = PayloadSources.VIEW; + this.dispatch(action); +}; +AppDispatcher.dispatchServerAction = function (action) { + action.source = PayloadSources.SERVER; + this.dispatch(action); +}; + +module.exports = { + AppDispatcher: AppDispatcher +}; + + +},{"flux":"flux"}],22:[function(require,module,exports){ +"use strict"; + +module.exports = (function () { + /* + * Generated by PEG.js 0.8.0. + * + * http://pegjs.majda.cz/ + */ + + function peg$subclass(child, parent) { + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + + function SyntaxError(message, expected, found, offset, line, column) { + this.message = message; + this.expected = expected; + this.found = found; + this.offset = offset; + this.line = line; + this.column = column; + + this.name = "SyntaxError"; + } + + peg$subclass(SyntaxError, Error); + + function parse(input) { + var options = arguments.length > 1 ? arguments[1] : {}, + peg$FAILED = {}, + peg$startRuleFunctions = { start: peg$parsestart }, + peg$startRuleFunction = peg$parsestart, + peg$c0 = { type: "other", description: "filter expression" }, + peg$c1 = peg$FAILED, + peg$c2 = function peg$c2(orExpr) { + return orExpr; + }, + peg$c3 = [], + peg$c4 = function peg$c4() { + return trueFilter; + }, + peg$c5 = { type: "other", description: "whitespace" }, + peg$c6 = /^[ \t\n\r]/, + peg$c7 = { type: "class", value: "[ \\t\\n\\r]", description: "[ \\t\\n\\r]" }, + peg$c8 = { type: "other", description: "control character" }, + peg$c9 = /^[|&!()~"]/, + peg$c10 = { type: "class", value: "[|&!()~\"]", description: "[|&!()~\"]" }, + peg$c11 = { type: "other", description: "optional whitespace" }, + peg$c12 = "|", + peg$c13 = { type: "literal", value: "|", description: "\"|\"" }, + peg$c14 = function peg$c14(first, second) { + return or(first, second); + }, + peg$c15 = "&", + peg$c16 = { type: "literal", value: "&", description: "\"&\"" }, + peg$c17 = function peg$c17(first, second) { + return and(first, second); + }, + peg$c18 = "!", + peg$c19 = { type: "literal", value: "!", description: "\"!\"" }, + peg$c20 = function peg$c20(expr) { + return not(expr); + }, + peg$c21 = "(", + peg$c22 = { type: "literal", value: "(", description: "\"(\"" }, + peg$c23 = ")", + peg$c24 = { type: "literal", value: ")", description: "\")\"" }, + peg$c25 = function peg$c25(expr) { + return binding(expr); + }, + peg$c26 = "~a", + peg$c27 = { type: "literal", value: "~a", description: "\"~a\"" }, + peg$c28 = function peg$c28() { + return assetFilter; + }, + peg$c29 = "~e", + peg$c30 = { type: "literal", value: "~e", description: "\"~e\"" }, + peg$c31 = function peg$c31() { + return errorFilter; + }, + peg$c32 = "~q", + peg$c33 = { type: "literal", value: "~q", description: "\"~q\"" }, + peg$c34 = function peg$c34() { + return noResponseFilter; + }, + peg$c35 = "~s", + peg$c36 = { type: "literal", value: "~s", description: "\"~s\"" }, + peg$c37 = function peg$c37() { + return responseFilter; + }, + peg$c38 = "true", + peg$c39 = { type: "literal", value: "true", description: "\"true\"" }, + peg$c40 = function peg$c40() { + return trueFilter; + }, + peg$c41 = "false", + peg$c42 = { type: "literal", value: "false", description: "\"false\"" }, + peg$c43 = function peg$c43() { + return falseFilter; + }, + peg$c44 = "~c", + peg$c45 = { type: "literal", value: "~c", description: "\"~c\"" }, + peg$c46 = function peg$c46(s) { + return responseCode(s); + }, + peg$c47 = "~d", + peg$c48 = { type: "literal", value: "~d", description: "\"~d\"" }, + peg$c49 = function peg$c49(s) { + return domain(s); + }, + peg$c50 = "~h", + peg$c51 = { type: "literal", value: "~h", description: "\"~h\"" }, + peg$c52 = function peg$c52(s) { + return header(s); + }, + peg$c53 = "~hq", + peg$c54 = { type: "literal", value: "~hq", description: "\"~hq\"" }, + peg$c55 = function peg$c55(s) { + return requestHeader(s); + }, + peg$c56 = "~hs", + peg$c57 = { type: "literal", value: "~hs", description: "\"~hs\"" }, + peg$c58 = function peg$c58(s) { + return responseHeader(s); + }, + peg$c59 = "~m", + peg$c60 = { type: "literal", value: "~m", description: "\"~m\"" }, + peg$c61 = function peg$c61(s) { + return method(s); + }, + peg$c62 = "~t", + peg$c63 = { type: "literal", value: "~t", description: "\"~t\"" }, + peg$c64 = function peg$c64(s) { + return contentType(s); + }, + peg$c65 = "~tq", + peg$c66 = { type: "literal", value: "~tq", description: "\"~tq\"" }, + peg$c67 = function peg$c67(s) { + return requestContentType(s); + }, + peg$c68 = "~ts", + peg$c69 = { type: "literal", value: "~ts", description: "\"~ts\"" }, + peg$c70 = function peg$c70(s) { + return responseContentType(s); + }, + peg$c71 = "~u", + peg$c72 = { type: "literal", value: "~u", description: "\"~u\"" }, + peg$c73 = function peg$c73(s) { + return url(s); + }, + peg$c74 = { type: "other", description: "integer" }, + peg$c75 = null, + peg$c76 = /^['"]/, + peg$c77 = { type: "class", value: "['\"]", description: "['\"]" }, + peg$c78 = /^[0-9]/, + peg$c79 = { type: "class", value: "[0-9]", description: "[0-9]" }, + peg$c80 = function peg$c80(digits) { + return parseInt(digits.join(""), 10); + }, + peg$c81 = { type: "other", description: "string" }, + peg$c82 = "\"", + peg$c83 = { type: "literal", value: "\"", description: "\"\\\"\"" }, + peg$c84 = function peg$c84(chars) { + return chars.join(""); + }, + peg$c85 = "'", + peg$c86 = { type: "literal", value: "'", description: "\"'\"" }, + peg$c87 = void 0, + peg$c88 = /^["\\]/, + peg$c89 = { type: "class", value: "[\"\\\\]", description: "[\"\\\\]" }, + peg$c90 = { type: "any", description: "any character" }, + peg$c91 = function peg$c91(char) { + return char; + }, + peg$c92 = "\\", + peg$c93 = { type: "literal", value: "\\", description: "\"\\\\\"" }, + peg$c94 = /^['\\]/, + peg$c95 = { type: "class", value: "['\\\\]", description: "['\\\\]" }, + peg$c96 = /^['"\\]/, + peg$c97 = { type: "class", value: "['\"\\\\]", description: "['\"\\\\]" }, + peg$c98 = "n", + peg$c99 = { type: "literal", value: "n", description: "\"n\"" }, + peg$c100 = function peg$c100() { + return "\n"; + }, + peg$c101 = "r", + peg$c102 = { type: "literal", value: "r", description: "\"r\"" }, + peg$c103 = function peg$c103() { + return "\r"; + }, + peg$c104 = "t", + peg$c105 = { type: "literal", value: "t", description: "\"t\"" }, + peg$c106 = function peg$c106() { + return "\t"; + }, + peg$currPos = 0, + peg$reportedPos = 0, + peg$cachedPos = 0, + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }, + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$reportedPos, peg$currPos); + } + + function offset() { + return peg$reportedPos; + } + + function line() { + return peg$computePosDetails(peg$reportedPos).line; + } + + function column() { + return peg$computePosDetails(peg$reportedPos).column; + } + + function expected(description) { + throw peg$buildException(null, [{ type: "other", description: description }], peg$reportedPos); + } + + function error(message) { + throw peg$buildException(message, null, peg$reportedPos); + } + + function peg$computePosDetails(pos) { + function advance(details, startPos, endPos) { + var p, ch; + + for (p = startPos; p < endPos; p++) { + ch = input.charAt(p); + if (ch === "\n") { + if (!details.seenCR) { + details.line++; + } + details.column = 1; + details.seenCR = false; + } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { + details.line++; + details.column = 1; + details.seenCR = true; + } else { + details.column++; + details.seenCR = false; + } + } + } + + if (peg$cachedPos !== pos) { + if (peg$cachedPos > pos) { + peg$cachedPos = 0; + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }; + } + advance(peg$cachedPosDetails, peg$cachedPos, pos); + peg$cachedPos = pos; + } + + return peg$cachedPosDetails; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { + return; + } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildException(message, expected, pos) { + function cleanupExpected(expected) { + var i = 1; + + expected.sort(function (a, b) { + if (a.description < b.description) { + return -1; + } else if (a.description > b.description) { + return 1; + } else { + return 0; + } + }); + + while (i < expected.length) { + if (expected[i - 1] === expected[i]) { + expected.splice(i, 1); + } else { + i++; + } + } + } + + function buildMessage(expected, found) { + function stringEscape(s) { + function hex(ch) { + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + + return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\x08/g, '\\b').replace(/\t/g, '\\t').replace(/\n/g, '\\n').replace(/\f/g, '\\f').replace(/\r/g, '\\r').replace(/[\x00-\x07\x0B\x0E\x0F]/g, function (ch) { + return '\\x0' + hex(ch); + }).replace(/[\x10-\x1F\x80-\xFF]/g, function (ch) { + return '\\x' + hex(ch); + }).replace(/[\u0180-\u0FFF]/g, function (ch) { + return "\\u0" + hex(ch); + }).replace(/[\u1080-\uFFFF]/g, function (ch) { + return "\\u" + hex(ch); + }); + } + + var expectedDescs = new Array(expected.length), + expectedDesc, + foundDesc, + i; + + for (i = 0; i < expected.length; i++) { + expectedDescs[i] = expected[i].description; + } + + expectedDesc = expected.length > 1 ? expectedDescs.slice(0, -1).join(", ") + " or " + expectedDescs[expected.length - 1] : expectedDescs[0]; + + foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; + + return "Expected " + expectedDesc + " but " + foundDesc + " found."; + } + + var posDetails = peg$computePosDetails(pos), + found = pos < input.length ? input.charAt(pos) : null; + + if (expected !== null) { + cleanupExpected(expected); + } + + return new SyntaxError(message !== null ? message : buildMessage(expected, found), expected, found, pos, posDetails.line, posDetails.column); + } + + function peg$parsestart() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parse__(); + if (s1 !== peg$FAILED) { + s2 = peg$parseOrExpr(); + if (s2 !== peg$FAILED) { + s3 = peg$parse__(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c2(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = []; + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c4(); + } + s0 = s1; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c0); + } + } + + return s0; + } + + function peg$parsews() { + var s0, s1; + + peg$silentFails++; + if (peg$c6.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c7); + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c5); + } + } + + return s0; + } + + function peg$parsecc() { + var s0, s1; + + peg$silentFails++; + if (peg$c9.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c10); + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c8); + } + } + + return s0; + } + + function peg$parse__() { + var s0, s1; + + peg$silentFails++; + s0 = []; + s1 = peg$parsews(); + while (s1 !== peg$FAILED) { + s0.push(s1); + s1 = peg$parsews(); + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c11); + } + } + + return s0; + } + + function peg$parseOrExpr() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseAndExpr(); + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 124) { + s3 = peg$c12; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c13); + } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + if (s4 !== peg$FAILED) { + s5 = peg$parseOrExpr(); + if (s5 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c14(s1, s5); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseAndExpr(); + } + + return s0; + } + + function peg$parseAndExpr() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseNotExpr(); + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 38) { + s3 = peg$c15; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c16); + } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + if (s4 !== peg$FAILED) { + s5 = peg$parseAndExpr(); + if (s5 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c17(s1, s5); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseNotExpr(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseAndExpr(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c17(s1, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseNotExpr(); + } + } + + return s0; + } + + function peg$parseNotExpr() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 33) { + s1 = peg$c18; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c19); + } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + s3 = peg$parseNotExpr(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c20(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseBindingExpr(); + } + + return s0; + } + + function peg$parseBindingExpr() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c21; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c22); + } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + s3 = peg$parseOrExpr(); + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s5 = peg$c23; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c24); + } + } + if (s5 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c25(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseExpr(); + } + + return s0; + } + + function peg$parseExpr() { + var s0; + + s0 = peg$parseNullaryExpr(); + if (s0 === peg$FAILED) { + s0 = peg$parseUnaryExpr(); + } + + return s0; + } + + function peg$parseNullaryExpr() { + var s0, s1; + + s0 = peg$parseBooleanLiteral(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c26) { + s1 = peg$c26; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c27); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c28(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c29) { + s1 = peg$c29; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c30); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c31(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c32) { + s1 = peg$c32; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c33); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c34(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c35) { + s1 = peg$c35; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c36); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c37(); + } + s0 = s1; + } + } + } + } + + return s0; + } + + function peg$parseBooleanLiteral() { + var s0, s1; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c38) { + s1 = peg$c38; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c39); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c40(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c41) { + s1 = peg$c41; + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c42); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c43(); + } + s0 = s1; + } + + return s0; + } + + function peg$parseUnaryExpr() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c44) { + s1 = peg$c44; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c45); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseIntegerLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c46(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c47) { + s1 = peg$c47; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c48); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c49(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c50) { + s1 = peg$c50; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c51); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c52(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c53) { + s1 = peg$c53; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c54); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c55(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c56) { + s1 = peg$c56; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c57); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c58(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c59) { + s1 = peg$c59; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c60); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c61(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c62) { + s1 = peg$c62; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c63); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c64(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c65) { + s1 = peg$c65; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c66); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c67(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c68) { + s1 = peg$c68; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c69); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c70(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c71) { + s1 = peg$c71; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c72); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c73(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseStringLiteral(); + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c73(s1); + } + s0 = s1; + } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parseIntegerLiteral() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (peg$c76.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c77); + } + } + if (s1 === peg$FAILED) { + s1 = peg$c75; + } + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c78.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c79); + } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c78.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c79); + } + } + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + if (peg$c76.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c77); + } + } + if (s3 === peg$FAILED) { + s3 = peg$c75; + } + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c80(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c74); + } + } + + return s0; + } + + function peg$parseStringLiteral() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c82; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c83); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseDoubleStringChar(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseDoubleStringChar(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c82; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c83); + } + } + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c84(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 39) { + s1 = peg$c85; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c86); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSingleStringChar(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSingleStringChar(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 39) { + s3 = peg$c85; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c86); + } + } + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c84(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + s2 = peg$parsecc(); + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseUnquotedStringChar(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseUnquotedStringChar(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c84(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c81); + } + } + + return s0; + } + + function peg$parseDoubleStringChar() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + if (peg$c88.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c89); + } + } + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c90); + } + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c92; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c93); + } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseEscapeSequence(); + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseSingleStringChar() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + if (peg$c94.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c95); + } + } + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c90); + } + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c92; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c93); + } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseEscapeSequence(); + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseUnquotedStringChar() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + s2 = peg$parsews(); + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c90); + } + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseEscapeSequence() { + var s0, s1; + + if (peg$c96.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c97); + } + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 110) { + s1 = peg$c98; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c99); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c100(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 114) { + s1 = peg$c101; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c102); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c103(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 116) { + s1 = peg$c104; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c105); + } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c106(); + } + s0 = s1; + } + } + } + + return s0; + } + + var flowutils = require("../flow/utils.js"); + + function or(first, second) { + // Add explicit function names to ease debugging. + function orFilter() { + return first.apply(this, arguments) || second.apply(this, arguments); + } + orFilter.desc = first.desc + " or " + second.desc; + return orFilter; + } + function and(first, second) { + function andFilter() { + return first.apply(this, arguments) && second.apply(this, arguments); + } + andFilter.desc = first.desc + " and " + second.desc; + return andFilter; + } + function not(expr) { + function notFilter() { + return !expr.apply(this, arguments); + } + notFilter.desc = "not " + expr.desc; + return notFilter; + } + function binding(expr) { + function bindingFilter() { + return expr.apply(this, arguments); + } + bindingFilter.desc = "(" + expr.desc + ")"; + return bindingFilter; + } + function trueFilter(flow) { + return true; + } + trueFilter.desc = "true"; + function falseFilter(flow) { + return false; + } + falseFilter.desc = "false"; + + var ASSET_TYPES = [new RegExp("text/javascript"), new RegExp("application/x-javascript"), new RegExp("application/javascript"), new RegExp("text/css"), new RegExp("image/.*"), new RegExp("application/x-shockwave-flash")]; + function assetFilter(flow) { + if (flow.response) { + var ct = flowutils.ResponseUtils.getContentType(flow.response); + var i = ASSET_TYPES.length; + while (i--) { + if (ASSET_TYPES[i].test(ct)) { + return true; + } + } + } + return false; + } + assetFilter.desc = "is asset"; + function responseCode(code) { + function responseCodeFilter(flow) { + return flow.response && flow.response.status_code === code; + } + responseCodeFilter.desc = "resp. code is " + code; + return responseCodeFilter; + } + function domain(regex) { + regex = new RegExp(regex, "i"); + function domainFilter(flow) { + return flow.request && regex.test(flow.request.host); + } + domainFilter.desc = "domain matches " + regex; + return domainFilter; + } + function errorFilter(flow) { + return !!flow.error; + } + errorFilter.desc = "has error"; + function header(regex) { + regex = new RegExp(regex, "i"); + function headerFilter(flow) { + return flow.request && flowutils.RequestUtils.match_header(flow.request, regex) || flow.response && flowutils.ResponseUtils.match_header(flow.response, regex); + } + headerFilter.desc = "header matches " + regex; + return headerFilter; + } + function requestHeader(regex) { + regex = new RegExp(regex, "i"); + function requestHeaderFilter(flow) { + return flow.request && flowutils.RequestUtils.match_header(flow.request, regex); + } + requestHeaderFilter.desc = "req. header matches " + regex; + return requestHeaderFilter; + } + function responseHeader(regex) { + regex = new RegExp(regex, "i"); + function responseHeaderFilter(flow) { + return flow.response && flowutils.ResponseUtils.match_header(flow.response, regex); + } + responseHeaderFilter.desc = "resp. header matches " + regex; + return responseHeaderFilter; + } + function method(regex) { + regex = new RegExp(regex, "i"); + function methodFilter(flow) { + return flow.request && regex.test(flow.request.method); + } + methodFilter.desc = "method matches " + regex; + return methodFilter; + } + function noResponseFilter(flow) { + return flow.request && !flow.response; + } + noResponseFilter.desc = "has no response"; + function responseFilter(flow) { + return !!flow.response; + } + responseFilter.desc = "has response"; + + function contentType(regex) { + regex = new RegExp(regex, "i"); + function contentTypeFilter(flow) { + return flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request)) || flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response)); + } + contentTypeFilter.desc = "content type matches " + regex; + return contentTypeFilter; + } + function requestContentType(regex) { + regex = new RegExp(regex, "i"); + function requestContentTypeFilter(flow) { + return flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request)); + } + requestContentTypeFilter.desc = "req. content type matches " + regex; + return requestContentTypeFilter; + } + function responseContentType(regex) { + regex = new RegExp(regex, "i"); + function responseContentTypeFilter(flow) { + return flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response)); + } + responseContentTypeFilter.desc = "resp. content type matches " + regex; + return responseContentTypeFilter; + } + function url(regex) { + regex = new RegExp(regex, "i"); + function urlFilter(flow) { + return flow.request && regex.test(flowutils.RequestUtils.pretty_url(flow.request)); + } + urlFilter.desc = "url matches " + regex; + return urlFilter; + } + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail({ type: "end", description: "end of input" }); + } + + throw peg$buildException(null, peg$maxFailExpected, peg$maxFailPos); + } + } + + return { + SyntaxError: SyntaxError, + parse: parse + }; +})(); + + +},{"../flow/utils.js":23}],23:[function(require,module,exports){ +"use strict"; + +var _ = require("lodash"); +var $ = require("jquery"); + +var defaultPorts = { + "http": 80, + "https": 443 +}; + +var MessageUtils = { + getContentType: function getContentType(message) { + var ct = this.get_first_header(message, /^Content-Type$/i); + if (ct) { + return ct.split(";")[0].trim(); + } + }, + get_first_header: function get_first_header(message, regex) { + //FIXME: Cache Invalidation. + if (!message._headerLookups) Object.defineProperty(message, "_headerLookups", { + value: {}, + configurable: false, + enumerable: false, + writable: false + }); + if (!(regex in message._headerLookups)) { + var header; + for (var i = 0; i < message.headers.length; i++) { + if (!!message.headers[i][0].match(regex)) { + header = message.headers[i]; + break; + } + } + message._headerLookups[regex] = header ? header[1] : undefined; + } + return message._headerLookups[regex]; + }, + match_header: function match_header(message, regex) { + var headers = message.headers; + var i = headers.length; + while (i--) { + if (regex.test(headers[i].join(" "))) { + return headers[i]; + } + } + return false; + }, + getContentURL: function getContentURL(flow, message) { + if (message === flow.request) { + message = "request"; + } else if (message === flow.response) { + message = "response"; + } + return "/flows/" + flow.id + "/" + message + "/content"; + }, + getContent: function getContent(flow, message) { + var url = MessageUtils.getContentURL(flow, message); + return $.get(url); + } +}; + +var RequestUtils = _.extend(MessageUtils, { + pretty_host: function pretty_host(request) { + //FIXME: Add hostheader + return request.host; + }, + pretty_url: function pretty_url(request) { + var port = ""; + if (defaultPorts[request.scheme] !== request.port) { + port = ":" + request.port; + } + return request.scheme + "://" + this.pretty_host(request) + port + request.path; + } +}); + +var ResponseUtils = _.extend(MessageUtils, {}); + +var parseUrl_regex = /^(?:(https?):\/\/)?([^\/:]+)?(?::(\d+))?(\/.*)?$/i; +var parseUrl = function parseUrl(url) { + //there are many correct ways to parse a URL, + //however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-) + var parts = parseUrl_regex.exec(url); + if (!parts) { + return false; + } + + var scheme = parts[1], + host = parts[2], + port = parseInt(parts[3]), + path = parts[4]; + if (scheme) { + port = port || defaultPorts[scheme]; + } + var ret = {}; + if (scheme) { + ret.scheme = scheme; + } + if (host) { + ret.host = host; + } + if (port) { + ret.port = port; + } + if (path) { + ret.path = path; + } + return ret; +}; + +var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i; +var isValidHttpVersion = function isValidHttpVersion(httpVersion) { + return isValidHttpVersion_regex.test(httpVersion); +}; + +var parseHttpVersion = function parseHttpVersion(httpVersion) { + httpVersion = httpVersion.replace("HTTP/", "").split("."); + return _.map(httpVersion, function (x) { + return parseInt(x); + }); +}; + +module.exports = { + ResponseUtils: ResponseUtils, + RequestUtils: RequestUtils, + MessageUtils: MessageUtils, + parseUrl: parseUrl, + parseHttpVersion: parseHttpVersion, + isValidHttpVersion: isValidHttpVersion +}; + + +},{"jquery":"jquery","lodash":"lodash"}],24:[function(require,module,exports){ +"use strict"; + +var _ = require("lodash"); +var $ = require("jquery"); +var EventEmitter = require('events').EventEmitter; + +var utils = require("../utils.js"); +var actions = require("../actions.js"); +var dispatcher = require("../dispatcher.js"); + +function ListStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(ListStore.prototype, EventEmitter.prototype, { + add: function add(elem) { + if (elem.id in this._pos_map) { + return; + } + this._pos_map[elem.id] = this.list.length; + this.list.push(elem); + this.emit("add", elem); + }, + update: function update(elem) { + if (!(elem.id in this._pos_map)) { + return; + } + this.list[this._pos_map[elem.id]] = elem; + this.emit("update", elem); + }, + remove: function remove(elem_id) { + if (!(elem_id in this._pos_map)) { + return; + } + this.list.splice(this._pos_map[elem_id], 1); + this._build_map(); + this.emit("remove", elem_id); + }, + reset: function reset(elems) { + this.list = elems || []; + this._build_map(); + this.emit("recalculate"); + }, + _build_map: function _build_map() { + this._pos_map = {}; + for (var i = 0; i < this.list.length; i++) { + var elem = this.list[i]; + this._pos_map[elem.id] = i; + } + }, + get: function get(elem_id) { + return this.list[this._pos_map[elem_id]]; + }, + index: function index(elem_id) { + return this._pos_map[elem_id]; + } +}); + +function DictStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(DictStore.prototype, EventEmitter.prototype, { + update: function update(dict) { + _.merge(this.dict, dict); + this.emit("recalculate"); + }, + reset: function reset(dict) { + this.dict = dict || {}; + this.emit("recalculate"); + } +}); + +function LiveStoreMixin(type) { + this.type = type; + + this._updates_before_fetch = undefined; + this._fetchxhr = false; + + this.handle = this.handle.bind(this); + dispatcher.AppDispatcher.register(this.handle); + + // Avoid double-fetch on startup. + if (!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) { + this.fetch(); + } +} +_.extend(LiveStoreMixin.prototype, { + handle: function handle(event) { + if (event.type === actions.ActionTypes.CONNECTION_OPEN) { + return this.fetch(); + } + if (event.type === this.type) { + if (event.cmd === actions.StoreCmds.RESET) { + this.fetch(event.data); + } else if (this._updates_before_fetch) { + console.log("defer update", event); + this._updates_before_fetch.push(event); + } else { + this[event.cmd](event.data); + } + } + }, + close: function close() { + dispatcher.AppDispatcher.unregister(this.handle); + }, + fetch: function fetch(data) { + console.log("fetch " + this.type); + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._updates_before_fetch = []; // (JS: empty array is true) + if (data) { + this.handle_fetch(data); + } else { + this._fetchxhr = $.getJSON("/" + this.type).done((function (message) { + this.handle_fetch(message.data); + }).bind(this)).fail((function () { + EventLogActions.add_event("Could not fetch " + this.type); + }).bind(this)); + } + }, + handle_fetch: function handle_fetch(data) { + this._fetchxhr = false; + console.log(this.type + " fetched.", this._updates_before_fetch); + this.reset(data); + var updates = this._updates_before_fetch; + this._updates_before_fetch = false; + for (var i = 0; i < updates.length; i++) { + this.handle(updates[i]); + } + } +}); + +function LiveListStore(type) { + ListStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveListStore.prototype, ListStore.prototype, LiveStoreMixin.prototype); + +function LiveDictStore(type) { + DictStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveDictStore.prototype, DictStore.prototype, LiveStoreMixin.prototype); + +function FlowStore() { + return new LiveListStore(actions.ActionTypes.FLOW_STORE); +} + +function SettingsStore() { + return new LiveDictStore(actions.ActionTypes.SETTINGS_STORE); +} + +function EventLogStore() { + LiveListStore.call(this, actions.ActionTypes.EVENT_STORE); +} +_.extend(EventLogStore.prototype, LiveListStore.prototype, { + fetch: function fetch() { + LiveListStore.prototype.fetch.apply(this, arguments); + + // Make sure to display updates even if fetching all events failed. + // This way, we can send "fetch failed" log messages to the log. + if (this._fetchxhr) { + this._fetchxhr.fail((function () { + this.handle_fetch(null); + }).bind(this)); + } + } +}); + +module.exports = { + EventLogStore: EventLogStore, + SettingsStore: SettingsStore, + FlowStore: FlowStore +}; + + +},{"../actions.js":2,"../dispatcher.js":21,"../utils.js":26,"events":1,"jquery":"jquery","lodash":"lodash"}],25:[function(require,module,exports){ +"use strict"; + +var EventEmitter = require('events').EventEmitter; +var _ = require("lodash"); + +var utils = require("../utils.js"); + +function SortByStoreOrder(elem) { + return this.store.index(elem.id); +} + +var default_sort = SortByStoreOrder; +var default_filt = function default_filt(elem) { + return true; +}; + +function StoreView(store, filt, sortfun) { + EventEmitter.call(this); + + this.store = store; + + this.add = this.add.bind(this); + this.update = this.update.bind(this); + this.remove = this.remove.bind(this); + this.recalculate = this.recalculate.bind(this); + this.store.addListener("add", this.add); + this.store.addListener("update", this.update); + this.store.addListener("remove", this.remove); + this.store.addListener("recalculate", this.recalculate); + + this.recalculate(filt, sortfun); +} + +_.extend(StoreView.prototype, EventEmitter.prototype, { + close: function close() { + this.store.removeListener("add", this.add); + this.store.removeListener("update", this.update); + this.store.removeListener("remove", this.remove); + this.store.removeListener("recalculate", this.recalculate); + this.removeAllListeners(); + }, + recalculate: function recalculate(filt, sortfun) { + filt = filt || this.filt || default_filt; + sortfun = sortfun || this.sortfun || default_sort; + filt = filt.bind(this); + sortfun = sortfun.bind(this); + this.filt = filt; + this.sortfun = sortfun; + + this.list = this.store.list.filter(filt); + this.list.sort(function (a, b) { + var akey = sortfun(a); + var bkey = sortfun(b); + if (akey < bkey) { + return -1; + } else if (akey > bkey) { + return 1; + } else { + return 0; + } + }); + this.emit("recalculate"); + }, + index: function index(elem) { + return _.sortedIndex(this.list, elem, this.sortfun); + }, + add: function add(elem) { + if (this.filt(elem)) { + var idx = this.index(elem); + if (idx === this.list.length) { + //happens often, .push is way faster. + this.list.push(elem); + } else { + this.list.splice(idx, 0, elem); + } + this.emit("add", elem, idx); + } + }, + update: function update(elem) { + var idx; + var i = this.list.length; + // Search from the back, we usually update the latest entries. + while (i--) { + if (this.list[i].id === elem.id) { + idx = i; + break; + } + } + + if (idx === -1) { + //not contained in list + this.add(elem); + } else if (!this.filt(elem)) { + this.remove(elem.id); + } else { + if (this.sortfun(this.list[idx]) !== this.sortfun(elem)) { + //sortpos has changed + this.remove(this.list[idx]); + this.add(elem); + } else { + this.list[idx] = elem; + this.emit("update", elem, idx); + } + } + }, + remove: function remove(elem_id) { + var idx = this.list.length; + while (idx--) { + if (this.list[idx].id === elem_id) { + this.list.splice(idx, 1); + this.emit("remove", elem_id, idx); + break; + } + } + } +}); + +module.exports = { + StoreView: StoreView +}; + + +},{"../utils.js":26,"events":1,"lodash":"lodash"}],26:[function(require,module,exports){ +"use strict"; + +var $ = require("jquery"); +var _ = require("lodash"); +var actions = require("./actions.js"); + +window.$ = $; +window._ = _; +window.React = require("react"); + +var Key = { + UP: 38, + DOWN: 40, + PAGE_UP: 33, + PAGE_DOWN: 34, + HOME: 36, + END: 35, + LEFT: 37, + RIGHT: 39, + ENTER: 13, + ESC: 27, + TAB: 9, + SPACE: 32, + BACKSPACE: 8, + SHIFT: 16 +}; +// Add A-Z +for (var i = 65; i <= 90; i++) { + Key[String.fromCharCode(i)] = i; +} + +var formatSize = function formatSize(bytes) { + if (bytes === 0) return "0"; + var prefix = ["b", "kb", "mb", "gb", "tb"]; + for (var i = 0; i < prefix.length; i++) { + if (Math.pow(1024, i + 1) > bytes) { + break; + } + } + var precision; + if (bytes % Math.pow(1024, i) === 0) precision = 0;else precision = 1; + return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i]; +}; + +var formatTimeDelta = function formatTimeDelta(milliseconds) { + var time = milliseconds; + var prefix = ["ms", "s", "min", "h"]; + var div = [1000, 60, 60]; + var i = 0; + while (Math.abs(time) >= div[i] && i < div.length) { + time = time / div[i]; + i++; + } + return Math.round(time) + prefix[i]; +}; + +var formatTimeStamp = function formatTimeStamp(seconds) { + var ts = new Date(seconds * 1000).toISOString(); + return ts.replace("T", " ").replace("Z", ""); +}; + +// At some places, we need to sort strings alphabetically descending, +// but we can only provide a key function. +// This beauty "reverses" a JS string. +var end = String.fromCharCode(0xffff); +function reverseString(s) { + return String.fromCharCode.apply(String, _.map(s.split(""), function (c) { + return 0xffff - c.charCodeAt(0); + })) + end; +} + +function getCookie(name) { + var r = document.cookie.match(new RegExp("\\b" + name + "=([^;]*)\\b")); + return r ? r[1] : undefined; +} +var xsrf = $.param({ _xsrf: getCookie("_xsrf") }); + +//Tornado XSRF Protection. +$.ajaxPrefilter(function (options) { + if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") { + if (options.url.indexOf("?") === -1) { + options.url += "?" + xsrf; + } else { + options.url += "&" + xsrf; + } + } +}); +// Log AJAX Errors +$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) { + if (thrownError === "abort") { + return; + } + var message = jqXHR.responseText; + console.error(thrownError, message, arguments); + actions.EventLogActions.add_event(thrownError + ": " + message); + alert(message); +}); + +module.exports = { + formatSize: formatSize, + formatTimeDelta: formatTimeDelta, + formatTimeStamp: formatTimeStamp, + reverseString: reverseString, + Key: Key +}; + + +},{"./actions.js":2,"jquery":"jquery","lodash":"lodash","react":"react"}]},{},[3]) + + +//# sourceMappingURL=app.js.map diff --git a/mitmproxy/web/static/fonts/fontawesome-webfont.eot b/mitmproxy/web/static/fonts/fontawesome-webfont.eot new file mode 100644 index 00000000..84677bc0 Binary files /dev/null and b/mitmproxy/web/static/fonts/fontawesome-webfont.eot differ diff --git a/mitmproxy/web/static/fonts/fontawesome-webfont.svg b/mitmproxy/web/static/fonts/fontawesome-webfont.svg new file mode 100644 index 00000000..d907b25a --- /dev/null +++ b/mitmproxy/web/static/fonts/fontawesome-webfont.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mitmproxy/web/static/fonts/fontawesome-webfont.ttf b/mitmproxy/web/static/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..96a3639c Binary files /dev/null and b/mitmproxy/web/static/fonts/fontawesome-webfont.ttf differ diff --git a/mitmproxy/web/static/fonts/fontawesome-webfont.woff b/mitmproxy/web/static/fonts/fontawesome-webfont.woff new file mode 100644 index 00000000..628b6a52 Binary files /dev/null and b/mitmproxy/web/static/fonts/fontawesome-webfont.woff differ diff --git a/mitmproxy/web/static/images/chrome-devtools/LICENSE b/mitmproxy/web/static/images/chrome-devtools/LICENSE new file mode 100644 index 00000000..6e4f8b9f --- /dev/null +++ b/mitmproxy/web/static/images/chrome-devtools/LICENSE @@ -0,0 +1,30 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// +// The Chromium Authors can be found at +// http://src.chromium.org/svn/trunk/src/AUTHORS +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/mitmproxy/web/static/images/chrome-devtools/resourceCSSIcon.png b/mitmproxy/web/static/images/chrome-devtools/resourceCSSIcon.png new file mode 100644 index 00000000..18828d06 Binary files /dev/null and b/mitmproxy/web/static/images/chrome-devtools/resourceCSSIcon.png differ diff --git a/mitmproxy/web/static/images/chrome-devtools/resourceDocumentIcon.png b/mitmproxy/web/static/images/chrome-devtools/resourceDocumentIcon.png new file mode 100644 index 00000000..fdc10e47 Binary files /dev/null and b/mitmproxy/web/static/images/chrome-devtools/resourceDocumentIcon.png differ diff --git a/mitmproxy/web/static/images/chrome-devtools/resourceJSIcon.png b/mitmproxy/web/static/images/chrome-devtools/resourceJSIcon.png new file mode 100644 index 00000000..c1b72189 Binary files /dev/null and b/mitmproxy/web/static/images/chrome-devtools/resourceJSIcon.png differ diff --git a/mitmproxy/web/static/images/chrome-devtools/resourcePlainIcon.png b/mitmproxy/web/static/images/chrome-devtools/resourcePlainIcon.png new file mode 100644 index 00000000..8c82a4c7 Binary files /dev/null and b/mitmproxy/web/static/images/chrome-devtools/resourcePlainIcon.png differ diff --git a/mitmproxy/web/static/images/resourceExecutableIcon.png b/mitmproxy/web/static/images/resourceExecutableIcon.png new file mode 100644 index 00000000..fa70c2fd Binary files /dev/null and b/mitmproxy/web/static/images/resourceExecutableIcon.png differ diff --git a/mitmproxy/web/static/images/resourceFlashIcon.png b/mitmproxy/web/static/images/resourceFlashIcon.png new file mode 100644 index 00000000..ead5a4d0 Binary files /dev/null and b/mitmproxy/web/static/images/resourceFlashIcon.png differ diff --git a/mitmproxy/web/static/images/resourceImageIcon.png b/mitmproxy/web/static/images/resourceImageIcon.png new file mode 100644 index 00000000..23163042 Binary files /dev/null and b/mitmproxy/web/static/images/resourceImageIcon.png differ diff --git a/mitmproxy/web/static/images/resourceJavaIcon.png b/mitmproxy/web/static/images/resourceJavaIcon.png new file mode 100644 index 00000000..553b3391 Binary files /dev/null and b/mitmproxy/web/static/images/resourceJavaIcon.png differ diff --git a/mitmproxy/web/static/images/resourceNotModifiedIcon.png b/mitmproxy/web/static/images/resourceNotModifiedIcon.png new file mode 100644 index 00000000..9c6a879d Binary files /dev/null and b/mitmproxy/web/static/images/resourceNotModifiedIcon.png differ diff --git a/mitmproxy/web/static/images/resourceRedirectIcon.png b/mitmproxy/web/static/images/resourceRedirectIcon.png new file mode 100644 index 00000000..58fe3ac1 Binary files /dev/null and b/mitmproxy/web/static/images/resourceRedirectIcon.png differ diff --git a/mitmproxy/web/static/vendor.css b/mitmproxy/web/static/vendor.css new file mode 100644 index 00000000..4ed1f0b8 --- /dev/null +++ b/mitmproxy/web/static/vendor.css @@ -0,0 +1,8437 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +mark { + background: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + box-sizing: content-box; + height: 0; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-appearance: textfield; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + background: transparent !important; + color: #000 !important; + box-shadow: none !important; + text-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk:before { + content: "\2a"; +} +.glyphicon-plus:before { + content: "\2b"; +} +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "\20ac"; +} +.glyphicon-minus:before { + content: "\2212"; +} +.glyphicon-cloud:before { + content: "\2601"; +} +.glyphicon-envelope:before { + content: "\2709"; +} +.glyphicon-pencil:before { + content: "\270f"; +} +.glyphicon-glass:before { + content: "\e001"; +} +.glyphicon-music:before { + content: "\e002"; +} +.glyphicon-search:before { + content: "\e003"; +} +.glyphicon-heart:before { + content: "\e005"; +} +.glyphicon-star:before { + content: "\e006"; +} +.glyphicon-star-empty:before { + content: "\e007"; +} +.glyphicon-user:before { + content: "\e008"; +} +.glyphicon-film:before { + content: "\e009"; +} +.glyphicon-th-large:before { + content: "\e010"; +} +.glyphicon-th:before { + content: "\e011"; +} +.glyphicon-th-list:before { + content: "\e012"; +} +.glyphicon-ok:before { + content: "\e013"; +} +.glyphicon-remove:before { + content: "\e014"; +} +.glyphicon-zoom-in:before { + content: "\e015"; +} +.glyphicon-zoom-out:before { + content: "\e016"; +} +.glyphicon-off:before { + content: "\e017"; +} +.glyphicon-signal:before { + content: "\e018"; +} +.glyphicon-cog:before { + content: "\e019"; +} +.glyphicon-trash:before { + content: "\e020"; +} +.glyphicon-home:before { + content: "\e021"; +} +.glyphicon-file:before { + content: "\e022"; +} +.glyphicon-time:before { + content: "\e023"; +} +.glyphicon-road:before { + content: "\e024"; +} +.glyphicon-download-alt:before { + content: "\e025"; +} +.glyphicon-download:before { + content: "\e026"; +} +.glyphicon-upload:before { + content: "\e027"; +} +.glyphicon-inbox:before { + content: "\e028"; +} +.glyphicon-play-circle:before { + content: "\e029"; +} +.glyphicon-repeat:before { + content: "\e030"; +} +.glyphicon-refresh:before { + content: "\e031"; +} +.glyphicon-list-alt:before { + content: "\e032"; +} +.glyphicon-lock:before { + content: "\e033"; +} +.glyphicon-flag:before { + content: "\e034"; +} +.glyphicon-headphones:before { + content: "\e035"; +} +.glyphicon-volume-off:before { + content: "\e036"; +} +.glyphicon-volume-down:before { + content: "\e037"; +} +.glyphicon-volume-up:before { + content: "\e038"; +} +.glyphicon-qrcode:before { + content: "\e039"; +} +.glyphicon-barcode:before { + content: "\e040"; +} +.glyphicon-tag:before { + content: "\e041"; +} +.glyphicon-tags:before { + content: "\e042"; +} +.glyphicon-book:before { + content: "\e043"; +} +.glyphicon-bookmark:before { + content: "\e044"; +} +.glyphicon-print:before { + content: "\e045"; +} +.glyphicon-camera:before { + content: "\e046"; +} +.glyphicon-font:before { + content: "\e047"; +} +.glyphicon-bold:before { + content: "\e048"; +} +.glyphicon-italic:before { + content: "\e049"; +} +.glyphicon-text-height:before { + content: "\e050"; +} +.glyphicon-text-width:before { + content: "\e051"; +} +.glyphicon-align-left:before { + content: "\e052"; +} +.glyphicon-align-center:before { + content: "\e053"; +} +.glyphicon-align-right:before { + content: "\e054"; +} +.glyphicon-align-justify:before { + content: "\e055"; +} +.glyphicon-list:before { + content: "\e056"; +} +.glyphicon-indent-left:before { + content: "\e057"; +} +.glyphicon-indent-right:before { + content: "\e058"; +} +.glyphicon-facetime-video:before { + content: "\e059"; +} +.glyphicon-picture:before { + content: "\e060"; +} +.glyphicon-map-marker:before { + content: "\e062"; +} +.glyphicon-adjust:before { + content: "\e063"; +} +.glyphicon-tint:before { + content: "\e064"; +} +.glyphicon-edit:before { + content: "\e065"; +} +.glyphicon-share:before { + content: "\e066"; +} +.glyphicon-check:before { + content: "\e067"; +} +.glyphicon-move:before { + content: "\e068"; +} +.glyphicon-step-backward:before { + content: "\e069"; +} +.glyphicon-fast-backward:before { + content: "\e070"; +} +.glyphicon-backward:before { + content: "\e071"; +} +.glyphicon-play:before { + content: "\e072"; +} +.glyphicon-pause:before { + content: "\e073"; +} +.glyphicon-stop:before { + content: "\e074"; +} +.glyphicon-forward:before { + content: "\e075"; +} +.glyphicon-fast-forward:before { + content: "\e076"; +} +.glyphicon-step-forward:before { + content: "\e077"; +} +.glyphicon-eject:before { + content: "\e078"; +} +.glyphicon-chevron-left:before { + content: "\e079"; +} +.glyphicon-chevron-right:before { + content: "\e080"; +} +.glyphicon-plus-sign:before { + content: "\e081"; +} +.glyphicon-minus-sign:before { + content: "\e082"; +} +.glyphicon-remove-sign:before { + content: "\e083"; +} +.glyphicon-ok-sign:before { + content: "\e084"; +} +.glyphicon-question-sign:before { + content: "\e085"; +} +.glyphicon-info-sign:before { + content: "\e086"; +} +.glyphicon-screenshot:before { + content: "\e087"; +} +.glyphicon-remove-circle:before { + content: "\e088"; +} +.glyphicon-ok-circle:before { + content: "\e089"; +} +.glyphicon-ban-circle:before { + content: "\e090"; +} +.glyphicon-arrow-left:before { + content: "\e091"; +} +.glyphicon-arrow-right:before { + content: "\e092"; +} +.glyphicon-arrow-up:before { + content: "\e093"; +} +.glyphicon-arrow-down:before { + content: "\e094"; +} +.glyphicon-share-alt:before { + content: "\e095"; +} +.glyphicon-resize-full:before { + content: "\e096"; +} +.glyphicon-resize-small:before { + content: "\e097"; +} +.glyphicon-exclamation-sign:before { + content: "\e101"; +} +.glyphicon-gift:before { + content: "\e102"; +} +.glyphicon-leaf:before { + content: "\e103"; +} +.glyphicon-fire:before { + content: "\e104"; +} +.glyphicon-eye-open:before { + content: "\e105"; +} +.glyphicon-eye-close:before { + content: "\e106"; +} +.glyphicon-warning-sign:before { + content: "\e107"; +} +.glyphicon-plane:before { + content: "\e108"; +} +.glyphicon-calendar:before { + content: "\e109"; +} +.glyphicon-random:before { + content: "\e110"; +} +.glyphicon-comment:before { + content: "\e111"; +} +.glyphicon-magnet:before { + content: "\e112"; +} +.glyphicon-chevron-up:before { + content: "\e113"; +} +.glyphicon-chevron-down:before { + content: "\e114"; +} +.glyphicon-retweet:before { + content: "\e115"; +} +.glyphicon-shopping-cart:before { + content: "\e116"; +} +.glyphicon-folder-close:before { + content: "\e117"; +} +.glyphicon-folder-open:before { + content: "\e118"; +} +.glyphicon-resize-vertical:before { + content: "\e119"; +} +.glyphicon-resize-horizontal:before { + content: "\e120"; +} +.glyphicon-hdd:before { + content: "\e121"; +} +.glyphicon-bullhorn:before { + content: "\e122"; +} +.glyphicon-bell:before { + content: "\e123"; +} +.glyphicon-certificate:before { + content: "\e124"; +} +.glyphicon-thumbs-up:before { + content: "\e125"; +} +.glyphicon-thumbs-down:before { + content: "\e126"; +} +.glyphicon-hand-right:before { + content: "\e127"; +} +.glyphicon-hand-left:before { + content: "\e128"; +} +.glyphicon-hand-up:before { + content: "\e129"; +} +.glyphicon-hand-down:before { + content: "\e130"; +} +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} +.glyphicon-globe:before { + content: "\e135"; +} +.glyphicon-wrench:before { + content: "\e136"; +} +.glyphicon-tasks:before { + content: "\e137"; +} +.glyphicon-filter:before { + content: "\e138"; +} +.glyphicon-briefcase:before { + content: "\e139"; +} +.glyphicon-fullscreen:before { + content: "\e140"; +} +.glyphicon-dashboard:before { + content: "\e141"; +} +.glyphicon-paperclip:before { + content: "\e142"; +} +.glyphicon-heart-empty:before { + content: "\e143"; +} +.glyphicon-link:before { + content: "\e144"; +} +.glyphicon-phone:before { + content: "\e145"; +} +.glyphicon-pushpin:before { + content: "\e146"; +} +.glyphicon-usd:before { + content: "\e148"; +} +.glyphicon-gbp:before { + content: "\e149"; +} +.glyphicon-sort:before { + content: "\e150"; +} +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} +.glyphicon-sort-by-order:before { + content: "\e153"; +} +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} +.glyphicon-unchecked:before { + content: "\e157"; +} +.glyphicon-expand:before { + content: "\e158"; +} +.glyphicon-collapse-down:before { + content: "\e159"; +} +.glyphicon-collapse-up:before { + content: "\e160"; +} +.glyphicon-log-in:before { + content: "\e161"; +} +.glyphicon-flash:before { + content: "\e162"; +} +.glyphicon-log-out:before { + content: "\e163"; +} +.glyphicon-new-window:before { + content: "\e164"; +} +.glyphicon-record:before { + content: "\e165"; +} +.glyphicon-save:before { + content: "\e166"; +} +.glyphicon-open:before { + content: "\e167"; +} +.glyphicon-saved:before { + content: "\e168"; +} +.glyphicon-import:before { + content: "\e169"; +} +.glyphicon-export:before { + content: "\e170"; +} +.glyphicon-send:before { + content: "\e171"; +} +.glyphicon-floppy-disk:before { + content: "\e172"; +} +.glyphicon-floppy-saved:before { + content: "\e173"; +} +.glyphicon-floppy-remove:before { + content: "\e174"; +} +.glyphicon-floppy-save:before { + content: "\e175"; +} +.glyphicon-floppy-open:before { + content: "\e176"; +} +.glyphicon-credit-card:before { + content: "\e177"; +} +.glyphicon-transfer:before { + content: "\e178"; +} +.glyphicon-cutlery:before { + content: "\e179"; +} +.glyphicon-header:before { + content: "\e180"; +} +.glyphicon-compressed:before { + content: "\e181"; +} +.glyphicon-earphone:before { + content: "\e182"; +} +.glyphicon-phone-alt:before { + content: "\e183"; +} +.glyphicon-tower:before { + content: "\e184"; +} +.glyphicon-stats:before { + content: "\e185"; +} +.glyphicon-sd-video:before { + content: "\e186"; +} +.glyphicon-hd-video:before { + content: "\e187"; +} +.glyphicon-subtitles:before { + content: "\e188"; +} +.glyphicon-sound-stereo:before { + content: "\e189"; +} +.glyphicon-sound-dolby:before { + content: "\e190"; +} +.glyphicon-sound-5-1:before { + content: "\e191"; +} +.glyphicon-sound-6-1:before { + content: "\e192"; +} +.glyphicon-sound-7-1:before { + content: "\e193"; +} +.glyphicon-copyright-mark:before { + content: "\e194"; +} +.glyphicon-registration-mark:before { + content: "\e195"; +} +.glyphicon-cloud-download:before { + content: "\e197"; +} +.glyphicon-cloud-upload:before { + content: "\e198"; +} +.glyphicon-tree-conifer:before { + content: "\e199"; +} +.glyphicon-tree-deciduous:before { + content: "\e200"; +} +.glyphicon-cd:before { + content: "\e201"; +} +.glyphicon-save-file:before { + content: "\e202"; +} +.glyphicon-open-file:before { + content: "\e203"; +} +.glyphicon-level-up:before { + content: "\e204"; +} +.glyphicon-copy:before { + content: "\e205"; +} +.glyphicon-paste:before { + content: "\e206"; +} +.glyphicon-alert:before { + content: "\e209"; +} +.glyphicon-equalizer:before { + content: "\e210"; +} +.glyphicon-king:before { + content: "\e211"; +} +.glyphicon-queen:before { + content: "\e212"; +} +.glyphicon-pawn:before { + content: "\e213"; +} +.glyphicon-bishop:before { + content: "\e214"; +} +.glyphicon-knight:before { + content: "\e215"; +} +.glyphicon-baby-formula:before { + content: "\e216"; +} +.glyphicon-tent:before { + content: "\26fa"; +} +.glyphicon-blackboard:before { + content: "\e218"; +} +.glyphicon-bed:before { + content: "\e219"; +} +.glyphicon-apple:before { + content: "\f8ff"; +} +.glyphicon-erase:before { + content: "\e221"; +} +.glyphicon-hourglass:before { + content: "\231b"; +} +.glyphicon-lamp:before { + content: "\e223"; +} +.glyphicon-duplicate:before { + content: "\e224"; +} +.glyphicon-piggy-bank:before { + content: "\e225"; +} +.glyphicon-scissors:before { + content: "\e226"; +} +.glyphicon-bitcoin:before { + content: "\e227"; +} +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} +.glyphicon-yen:before { + content: "\00a5"; +} +.glyphicon-jpy:before { + content: "\00a5"; +} +.glyphicon-ruble:before { + content: "\20bd"; +} +.glyphicon-rub:before { + content: "\20bd"; +} +.glyphicon-scale:before { + content: "\e230"; +} +.glyphicon-ice-lolly:before { + content: "\e231"; +} +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; +} +.glyphicon-education:before { + content: "\e233"; +} +.glyphicon-option-horizontal:before { + content: "\e234"; +} +.glyphicon-option-vertical:before { + content: "\e235"; +} +.glyphicon-menu-hamburger:before { + content: "\e236"; +} +.glyphicon-modal-window:before { + content: "\e237"; +} +.glyphicon-oil:before { + content: "\e238"; +} +.glyphicon-grain:before { + content: "\e239"; +} +.glyphicon-sunglasses:before { + content: "\e240"; +} +.glyphicon-text-size:before { + content: "\e241"; +} +.glyphicon-text-color:before { + content: "\e242"; +} +.glyphicon-text-background:before { + content: "\e243"; +} +.glyphicon-object-align-top:before { + content: "\e244"; +} +.glyphicon-object-align-bottom:before { + content: "\e245"; +} +.glyphicon-object-align-horizontal:before { + content: "\e246"; +} +.glyphicon-object-align-left:before { + content: "\e247"; +} +.glyphicon-object-align-vertical:before { + content: "\e248"; +} +.glyphicon-object-align-right:before { + content: "\e249"; +} +.glyphicon-triangle-right:before { + content: "\e250"; +} +.glyphicon-triangle-left:before { + content: "\e251"; +} +.glyphicon-triangle-bottom:before { + content: "\e252"; +} +.glyphicon-triangle-top:before { + content: "\e253"; +} +.glyphicon-console:before { + content: "\e254"; +} +.glyphicon-superscript:before { + content: "\e255"; +} +.glyphicon-subscript:before { + content: "\e256"; +} +.glyphicon-menu-left:before { + content: "\e257"; +} +.glyphicon-menu-right:before { + content: "\e258"; +} +.glyphicon-menu-down:before { + content: "\e259"; +} +.glyphicon-menu-up:before { + content: "\e260"; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +[role="button"] { + cursor: pointer; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + background-color: #fcf8e3; + padding: .2em; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:hover, +a.text-primary:focus { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:hover, +a.text-success:focus { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover, +a.text-info:focus { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover, +a.text-warning:focus { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover, +a.text-danger:focus { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + list-style: none; + margin-left: -5px; +} +.list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} +.row { + margin-left: -15px; + margin-right: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-left: 15px; + padding-right: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0%; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0%; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0%; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + overflow-x: auto; + min-height: 0.01%; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + padding: 0; + margin: 0; + border: 0; + min-width: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #eeeeee; + opacity: 1; +} +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"].form-control, + input[type="time"].form-control, + input[type="datetime-local"].form-control, + input[type="month"].form-control { + line-height: 34px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-left: -20px; + margin-top: 4px \9; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.form-control-static { + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; + min-height: 34px; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-left: 0; + padding-right: 0; +} +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.form-group-sm select.form-control { + height: 30px; + line-height: 30px; +} +.form-group-sm textarea.form-control, +.form-group-sm select[multiple].form-control { + height: auto; +} +.form-group-sm .form-control-static { + height: 30px; + min-height: 32px; + padding: 6px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-lg { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.form-group-lg select.form-control { + height: 46px; + line-height: 46px; +} +.form-group-lg textarea.form-control, +.form-group-lg select[multiple].form-control { + height: auto; +} +.form-group-lg .form-control-static { + height: 46px; + min-height: 38px; + padding: 11px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + border-color: #3c763d; + background-color: #dff0d8; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + border-color: #8a6d3b; + background-color: #fcf8e3; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + border-color: #a94442; + background-color: #f2dede; +} +.has-error .form-control-feedback { + color: #a94442; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + margin-top: 0; + margin-bottom: 0; + padding-top: 7px; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} +.form-horizontal .form-group { + margin-left: -15px; + margin-right: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + text-align: right; + margin-bottom: 0; + padding-top: 7px; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 14.333333px; + font-size: 18px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + font-size: 12px; + } +} +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} +.btn:active, +.btn.active { + outline: 0; + background-image: none; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; +} +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:focus, +.btn-default.focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary:focus, +.btn-primary.focus { + color: #fff; + background-color: #286090; + border-color: #122b40; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:focus, +.btn-success.focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #1b6d85; +} +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #985f0d; +} +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:focus, +.btn-danger.focus { + color: #fff; + background-color: #c9302c; + border-color: #761c19; +} +.btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + color: #337ab7; + font-weight: normal; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-property: height, visibility; + transition-property: height, visibility; + -webkit-transition-duration: 0.35s; + transition-duration: 0.35s; + -webkit-transition-timing-function: ease; + transition-timing-function: ease; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropup, +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 14px; + text-align: left; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + text-decoration: none; + color: #262626; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + outline: 0; + background-color: #337ab7; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + cursor: not-allowed; +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + left: auto; + right: 0; +} +.dropdown-menu-left { + left: 0; + right: auto; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px dashed; + border-bottom: 4px solid \9; + content: ""; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + left: auto; + right: 0; + } + .navbar-right .dropdown-menu-left { + left: 0; + right: auto; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn, +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-bottom-left-radius: 4px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + float: none; + display: table-cell; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555555; + text-align: center; + background-color: #eeeeee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + z-index: 2; + margin-left: -1px; +} +.nav { + margin-bottom: 0; + padding-left: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} +.nav > li.disabled > a { + color: #777777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777777; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eeeeee; + border-color: #337ab7; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555555; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + text-align: center; + margin-bottom: 5px; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #337ab7; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + text-align: center; + margin-bottom: 5px; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.navbar { + position: relative; + min-height: 32px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + overflow-x: visible; + padding-right: 15px; + padding-left: 15px; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-left: 0; + padding-right: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + padding: 6px 15px; + font-size: 18px; + line-height: 20px; + height: 32px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + margin-right: 15px; + padding: 9px 10px; + margin-top: -1px; + margin-bottom: -1px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 3px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 6px; + padding-bottom: 6px; + } +} +.navbar-form { + margin-left: -15px; + margin-right: -15px; + padding: 10px 15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + margin-top: -1px; + margin-bottom: -1px; +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: -1px; + margin-bottom: -1px; +} +.navbar-btn.btn-sm { + margin-top: 1px; + margin-bottom: 1px; +} +.navbar-btn.btn-xs { + margin-top: 5px; + margin-bottom: 5px; +} +.navbar-text { + margin-top: 6px; + margin-bottom: 6px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-left: 15px; + margin-right: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + float: left; + } + .navbar-right { + float: right !important; + float: right; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #ffffff; + border-color: #e0e0e0; +} +.navbar-default .navbar-brand { + color: #303030; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #161616; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #303030; +} +.navbar-default .navbar-nav > li > a { + color: #303030; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #eeeeee; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e0e0e0; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + background-color: #eeeeee; + color: #555; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #303030; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #eeeeee; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #303030; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-default .btn-link { + color: #303030; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + background-color: #080808; + color: #fff; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + content: "/\00a0"; + padding: 0 5px; + color: #ccc; +} +.breadcrumb > .active { + color: #777777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + line-height: 1.42857143; + text-decoration: none; + color: #337ab7; + background-color: #fff; + border: 1px solid #ddd; + margin-left: -1px; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + z-index: 3; + color: #23527c; + background-color: #eeeeee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; + cursor: default; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777777; + background-color: #fff; + border-color: #ddd; + cursor: not-allowed; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-bottom-right-radius: 6px; + border-top-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + list-style: none; + text-align: center; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777777; + background-color: #fff; + cursor: not-allowed; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777777; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} +.label-primary { + background-color: #337ab7; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #286090; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + color: #fff; + line-height: 1; + vertical-align: middle; + white-space: nowrap; + text-align: center; + background-color: #777777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge, +.btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #337ab7; + background-color: #fff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding-top: 30px; + padding-bottom: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #eeeeee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-left: 60px; + padding-right: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border 0.2s ease-in-out; + -o-transition: border 0.2s ease-in-out; + transition: border 0.2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-left: auto; + margin-right: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #337ab7; +} +.thumbnail .caption { + padding: 9px; + color: #333333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; + color: #3c763d; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; + color: #31708f; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + background-color: #fcf8e3; + border-color: #faebcc; + color: #8a6d3b; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + background-color: #f2dede; + border-color: #ebccd1; + color: #a94442; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + overflow: hidden; + height: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media, +.media-body { + zoom: 1; + overflow: hidden; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-object.img-thumbnail { + max-width: none; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + margin-bottom: 20px; + padding-left: 0; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +a.list-group-item, +button.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { + text-decoration: none; + color: #555; + background-color: #f5f5f5; +} +button.list-group-item { + width: 100%; + text-align: left; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + background-color: #eeeeee; + color: #777777; + cursor: not-allowed; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #c7ddef; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +button.list-group-item-success.active, +a.list-group-item-success.active:hover, +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +button.list-group-item-info.active, +a.list-group-item-info.active:hover, +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +button.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +button.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.panel-title > .small > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-left: 15px; + padding-right: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + border: 0; + margin-bottom: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #337ab7; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; +} +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + left: 0; + bottom: 0; + height: 100%; + width: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: 0.2; + filter: alpha(opacity=20); +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); +} +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} +.modal-open { + overflow: hidden; +} +.modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -moz-transition: -moz-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + background-clip: padding-box; + outline: 0; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} +.modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); +} +.modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); +} +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; + min-height: 16.42857143px; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-left: 5px; + margin-bottom: 0; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + font-size: 12px; + opacity: 0; + filter: alpha(opacity=0); +} +.tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); +} +.tooltip.top { + margin-top: -3px; + padding: 5px 0; +} +.tooltip.right { + margin-left: 3px; + padding: 0 5px; +} +.tooltip.bottom { + margin-top: 3px; + padding: 5px 0; +} +.tooltip.left { + margin-left: -3px; + padding: 0 5px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + bottom: 0; + right: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + font-size: 14px; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + margin: 0; + padding: 8px 14px; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + border-width: 10px; + content: ""; +} +.popover.top > .arrow { + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999999; + border-top-color: rgba(0, 0, 0, 0.25); + bottom: -11px; +} +.popover.top > .arrow:after { + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #fff; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + border-right-color: #999999; + border-right-color: rgba(0, 0, 0, 0.25); +} +.popover.right > .arrow:after { + content: " "; + left: 1px; + bottom: -10px; + border-left-width: 0; + border-right-color: #fff; +} +.popover.bottom > .arrow { + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999999; + border-bottom-color: rgba(0, 0, 0, 0.25); + top: -11px; +} +.popover.bottom > .arrow:after { + content: " "; + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999999; + border-left-color: rgba(0, 0, 0, 0.25); +} +.popover.left > .arrow:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: #fff; + bottom: -10px; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; +} +.carousel-inner > .item { + display: none; + position: relative; + -webkit-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform 0.6s ease-in-out; + -moz-transition: -moz-transform 0.6s ease-in-out; + -o-transition: -o-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + -moz-perspective: 1000px; + perspective: 1000px; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + left: 0; + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + left: 0; + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + left: 0; + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 15%; + opacity: 0.5; + filter: alpha(opacity=50); + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); +} +.carousel-control.right { + left: auto; + right: 0; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); +} +.carousel-control:hover, +.carousel-control:focus { + outline: 0; + color: #fff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + margin-top: -10px; + z-index: 5; + display: inline-block; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + line-height: 1; + font-family: serif; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid #fff; + border-radius: 10px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); +} +.carousel-indicators .active { + margin: 0; + width: 12px; + height: 12px; + background-color: #fff; +} +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -15px; + } + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-footer:before, +.modal-footer:after { + content: " "; + display: table; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-left: auto; + margin-right: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('fonts/fontawesome-webfont.eot?v=4.2.0'); + src: url('fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.3333333333333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.2857142857142858em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.142857142857143em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.142857142857143em; + top: 0.14285714285714285em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} + +/*# sourceMappingURL=vendor.css.map */ diff --git a/mitmproxy/web/static/vendor.js b/mitmproxy/web/static/vendor.js new file mode 100644 index 00000000..59d10445 --- /dev/null +++ b/mitmproxy/web/static/vendor.js @@ -0,0 +1,46760 @@ +require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + setTimeout(drainQueue, 0); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],2:[function(require,module,exports){ +(function (process){ +/** + * Copyright (c) 2014-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Dispatcher + * + * @preventMunge + */ + +'use strict'; + +exports.__esModule = true; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var invariant = require('fbjs/lib/invariant'); + +var _prefix = 'ID_'; + +/** + * Dispatcher is used to broadcast payloads to registered callbacks. This is + * different from generic pub-sub systems in two ways: + * + * 1) Callbacks are not subscribed to particular events. Every payload is + * dispatched to every registered callback. + * 2) Callbacks can be deferred in whole or part until other callbacks have + * been executed. + * + * For example, consider this hypothetical flight destination form, which + * selects a default city when a country is selected: + * + * var flightDispatcher = new Dispatcher(); + * + * // Keeps track of which country is selected + * var CountryStore = {country: null}; + * + * // Keeps track of which city is selected + * var CityStore = {city: null}; + * + * // Keeps track of the base flight price of the selected city + * var FlightPriceStore = {price: null} + * + * When a user changes the selected city, we dispatch the payload: + * + * flightDispatcher.dispatch({ + * actionType: 'city-update', + * selectedCity: 'paris' + * }); + * + * This payload is digested by `CityStore`: + * + * flightDispatcher.register(function(payload) { + * if (payload.actionType === 'city-update') { + * CityStore.city = payload.selectedCity; + * } + * }); + * + * When the user selects a country, we dispatch the payload: + * + * flightDispatcher.dispatch({ + * actionType: 'country-update', + * selectedCountry: 'australia' + * }); + * + * This payload is digested by both stores: + * + * CountryStore.dispatchToken = flightDispatcher.register(function(payload) { + * if (payload.actionType === 'country-update') { + * CountryStore.country = payload.selectedCountry; + * } + * }); + * + * When the callback to update `CountryStore` is registered, we save a reference + * to the returned token. Using this token with `waitFor()`, we can guarantee + * that `CountryStore` is updated before the callback that updates `CityStore` + * needs to query its data. + * + * CityStore.dispatchToken = flightDispatcher.register(function(payload) { + * if (payload.actionType === 'country-update') { + * // `CountryStore.country` may not be updated. + * flightDispatcher.waitFor([CountryStore.dispatchToken]); + * // `CountryStore.country` is now guaranteed to be updated. + * + * // Select the default city for the new country + * CityStore.city = getDefaultCityForCountry(CountryStore.country); + * } + * }); + * + * The usage of `waitFor()` can be chained, for example: + * + * FlightPriceStore.dispatchToken = + * flightDispatcher.register(function(payload) { + * switch (payload.actionType) { + * case 'country-update': + * case 'city-update': + * flightDispatcher.waitFor([CityStore.dispatchToken]); + * FlightPriceStore.price = + * getFlightPriceStore(CountryStore.country, CityStore.city); + * break; + * } + * }); + * + * The `country-update` payload will be guaranteed to invoke the stores' + * registered callbacks in order: `CountryStore`, `CityStore`, then + * `FlightPriceStore`. + */ + +var Dispatcher = (function () { + function Dispatcher() { + _classCallCheck(this, Dispatcher); + + this._callbacks = {}; + this._isDispatching = false; + this._isHandled = {}; + this._isPending = {}; + this._lastID = 1; + } + + /** + * Registers a callback to be invoked with every dispatched payload. Returns + * a token that can be used with `waitFor()`. + */ + + Dispatcher.prototype.register = function register(callback) { + var id = _prefix + this._lastID++; + this._callbacks[id] = callback; + return id; + }; + + /** + * Removes a callback based on its token. + */ + + Dispatcher.prototype.unregister = function unregister(id) { + !this._callbacks[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.unregister(...): `%s` does not map to a registered callback.', id) : invariant(false) : undefined; + delete this._callbacks[id]; + }; + + /** + * Waits for the callbacks specified to be invoked before continuing execution + * of the current callback. This method should only be used by a callback in + * response to a dispatched payload. + */ + + Dispatcher.prototype.waitFor = function waitFor(ids) { + !this._isDispatching ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): Must be invoked while dispatching.') : invariant(false) : undefined; + for (var ii = 0; ii < ids.length; ii++) { + var id = ids[ii]; + if (this._isPending[id]) { + !this._isHandled[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): Circular dependency detected while ' + 'waiting for `%s`.', id) : invariant(false) : undefined; + continue; + } + !this._callbacks[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): `%s` does not map to a registered callback.', id) : invariant(false) : undefined; + this._invokeCallback(id); + } + }; + + /** + * Dispatches a payload to all registered callbacks. + */ + + Dispatcher.prototype.dispatch = function dispatch(payload) { + !!this._isDispatching ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.') : invariant(false) : undefined; + this._startDispatching(payload); + try { + for (var id in this._callbacks) { + if (this._isPending[id]) { + continue; + } + this._invokeCallback(id); + } + } finally { + this._stopDispatching(); + } + }; + + /** + * Is this Dispatcher currently dispatching. + */ + + Dispatcher.prototype.isDispatching = function isDispatching() { + return this._isDispatching; + }; + + /** + * Call the callback stored with the given id. Also do some internal + * bookkeeping. + * + * @internal + */ + + Dispatcher.prototype._invokeCallback = function _invokeCallback(id) { + this._isPending[id] = true; + this._callbacks[id](this._pendingPayload); + this._isHandled[id] = true; + }; + + /** + * Set up bookkeeping needed when dispatching. + * + * @internal + */ + + Dispatcher.prototype._startDispatching = function _startDispatching(payload) { + for (var id in this._callbacks) { + this._isPending[id] = false; + this._isHandled[id] = false; + } + this._pendingPayload = payload; + this._isDispatching = true; + }; + + /** + * Clear bookkeeping used for dispatching. + * + * @internal + */ + + Dispatcher.prototype._stopDispatching = function _stopDispatching() { + delete this._pendingPayload; + this._isDispatching = false; + }; + + return Dispatcher; +})(); + +module.exports = Dispatcher; +}).call(this,require('_process')) + +},{"_process":1,"fbjs/lib/invariant":3}],3:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule invariant + */ + +"use strict"; + +/** + * Use invariant() to assert state which your program assumes to be true. + * + * Provide sprintf-style format (only %s is supported) and arguments + * to provide information about what broke and what you were + * expecting. + * + * The invariant message will be stripped in production, but the invariant + * will remain to ensure logic does not differ in production. + */ + +var invariant = function (condition, format, a, b, c, d, e, f) { + if (process.env.NODE_ENV !== 'production') { + if (format === undefined) { + throw new Error('invariant requires an error message argument'); + } + } + + if (!condition) { + var error; + if (format === undefined) { + error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.'); + } else { + var args = [a, b, c, d, e, f]; + var argIndex = 0; + error = new Error('Invariant Violation: ' + format.replace(/%s/g, function () { + return args[argIndex++]; + })); + } + + error.framesToPop = 1; // we don't care about invariant's own frame + throw error; + } +}; + +module.exports = invariant; +}).call(this,require('_process')) + +},{"_process":1}],4:[function(require,module,exports){ +/** + * Represents a cancellation caused by navigating away + * before the previous transition has fully resolved. + */ +"use strict"; + +function Cancellation() {} + +module.exports = Cancellation; +},{}],5:[function(require,module,exports){ +'use strict'; + +var invariant = require('react/lib/invariant'); +var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; + +var History = { + + /** + * The current number of entries in the history. + * + * Note: This property is read-only. + */ + length: 1, + + /** + * Sends the browser back one entry in the history. + */ + back: function back() { + invariant(canUseDOM, 'Cannot use History.back without a DOM'); + + // Do this first so that History.length will + // be accurate in location change listeners. + History.length -= 1; + + window.history.back(); + } + +}; + +module.exports = History; +},{"react/lib/ExecutionEnvironment":62,"react/lib/invariant":191}],6:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +/* jshint -W084 */ +var PathUtils = require('./PathUtils'); + +function deepSearch(route, pathname, query) { + // Check the subtree first to find the most deeply-nested match. + var childRoutes = route.childRoutes; + if (childRoutes) { + var match, childRoute; + for (var i = 0, len = childRoutes.length; i < len; ++i) { + childRoute = childRoutes[i]; + + if (childRoute.isDefault || childRoute.isNotFound) continue; // Check these in order later. + + if (match = deepSearch(childRoute, pathname, query)) { + // A route in the subtree matched! Add this route and we're done. + match.routes.unshift(route); + return match; + } + } + } + + // No child routes matched; try the default route. + var defaultRoute = route.defaultRoute; + if (defaultRoute && (params = PathUtils.extractParams(defaultRoute.path, pathname))) { + return new Match(pathname, params, query, [route, defaultRoute]); + } // Does the "not found" route match? + var notFoundRoute = route.notFoundRoute; + if (notFoundRoute && (params = PathUtils.extractParams(notFoundRoute.path, pathname))) { + return new Match(pathname, params, query, [route, notFoundRoute]); + } // Last attempt: check this route. + var params = PathUtils.extractParams(route.path, pathname); + if (params) { + return new Match(pathname, params, query, [route]); + }return null; +} + +var Match = (function () { + function Match(pathname, params, query, routes) { + _classCallCheck(this, Match); + + this.pathname = pathname; + this.params = params; + this.query = query; + this.routes = routes; + } + + _createClass(Match, null, [{ + key: 'findMatch', + + /** + * Attempts to match depth-first a route in the given route's + * subtree against the given path and returns the match if it + * succeeds, null if no match can be made. + */ + value: function findMatch(routes, path) { + var pathname = PathUtils.withoutQuery(path); + var query = PathUtils.extractQuery(path); + var match = null; + + for (var i = 0, len = routes.length; match == null && i < len; ++i) match = deepSearch(routes[i], pathname, query); + + return match; + } + }]); + + return Match; +})(); + +module.exports = Match; +},{"./PathUtils":8}],7:[function(require,module,exports){ +'use strict'; + +var PropTypes = require('./PropTypes'); + +/** + * A mixin for components that modify the URL. + * + * Example: + * + * var MyLink = React.createClass({ + * mixins: [ Router.Navigation ], + * handleClick(event) { + * event.preventDefault(); + * this.transitionTo('aRoute', { the: 'params' }, { the: 'query' }); + * }, + * render() { + * return ( + * Click me! + * ); + * } + * }); + */ +var Navigation = { + + contextTypes: { + router: PropTypes.router.isRequired + }, + + /** + * Returns an absolute URL path created from the given route + * name, URL parameters, and query values. + */ + makePath: function makePath(to, params, query) { + return this.context.router.makePath(to, params, query); + }, + + /** + * Returns a string that may safely be used as the href of a + * link to the route with the given name. + */ + makeHref: function makeHref(to, params, query) { + return this.context.router.makeHref(to, params, query); + }, + + /** + * Transitions to the URL specified in the arguments by pushing + * a new URL onto the history stack. + */ + transitionTo: function transitionTo(to, params, query) { + this.context.router.transitionTo(to, params, query); + }, + + /** + * Transitions to the URL specified in the arguments by replacing + * the current URL in the history stack. + */ + replaceWith: function replaceWith(to, params, query) { + this.context.router.replaceWith(to, params, query); + }, + + /** + * Transitions to the previous URL. + */ + goBack: function goBack() { + return this.context.router.goBack(); + } + +}; + +module.exports = Navigation; +},{"./PropTypes":9}],8:[function(require,module,exports){ +'use strict'; + +var invariant = require('react/lib/invariant'); +var assign = require('object-assign'); +var qs = require('qs'); + +var paramCompileMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|[*.()\[\]\\+|{}^$]/g; +var paramInjectMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$?]*[?]?)|[*]/g; +var paramInjectTrailingSlashMatcher = /\/\/\?|\/\?\/|\/\?/g; +var queryMatcher = /\?(.*)$/; + +var _compiledPatterns = {}; + +function compilePattern(pattern) { + if (!(pattern in _compiledPatterns)) { + var paramNames = []; + var source = pattern.replace(paramCompileMatcher, function (match, paramName) { + if (paramName) { + paramNames.push(paramName); + return '([^/?#]+)'; + } else if (match === '*') { + paramNames.push('splat'); + return '(.*?)'; + } else { + return '\\' + match; + } + }); + + _compiledPatterns[pattern] = { + matcher: new RegExp('^' + source + '$', 'i'), + paramNames: paramNames + }; + } + + return _compiledPatterns[pattern]; +} + +var PathUtils = { + + /** + * Returns true if the given path is absolute. + */ + isAbsolute: function isAbsolute(path) { + return path.charAt(0) === '/'; + }, + + /** + * Joins two URL paths together. + */ + join: function join(a, b) { + return a.replace(/\/*$/, '/') + b; + }, + + /** + * Returns an array of the names of all parameters in the given pattern. + */ + extractParamNames: function extractParamNames(pattern) { + return compilePattern(pattern).paramNames; + }, + + /** + * Extracts the portions of the given URL path that match the given pattern + * and returns an object of param name => value pairs. Returns null if the + * pattern does not match the given path. + */ + extractParams: function extractParams(pattern, path) { + var _compilePattern = compilePattern(pattern); + + var matcher = _compilePattern.matcher; + var paramNames = _compilePattern.paramNames; + + var match = path.match(matcher); + + if (!match) { + return null; + }var params = {}; + + paramNames.forEach(function (paramName, index) { + params[paramName] = match[index + 1]; + }); + + return params; + }, + + /** + * Returns a version of the given route path with params interpolated. Throws + * if there is a dynamic segment of the route path for which there is no param. + */ + injectParams: function injectParams(pattern, params) { + params = params || {}; + + var splatIndex = 0; + + return pattern.replace(paramInjectMatcher, function (match, paramName) { + paramName = paramName || 'splat'; + + // If param is optional don't check for existence + if (paramName.slice(-1) === '?') { + paramName = paramName.slice(0, -1); + + if (params[paramName] == null) return ''; + } else { + invariant(params[paramName] != null, 'Missing "%s" parameter for path "%s"', paramName, pattern); + } + + var segment; + if (paramName === 'splat' && Array.isArray(params[paramName])) { + segment = params[paramName][splatIndex++]; + + invariant(segment != null, 'Missing splat # %s for path "%s"', splatIndex, pattern); + } else { + segment = params[paramName]; + } + + return segment; + }).replace(paramInjectTrailingSlashMatcher, '/'); + }, + + /** + * Returns an object that is the result of parsing any query string contained + * in the given path, null if the path contains no query string. + */ + extractQuery: function extractQuery(path) { + var match = path.match(queryMatcher); + return match && qs.parse(match[1]); + }, + + /** + * Returns a version of the given path without the query string. + */ + withoutQuery: function withoutQuery(path) { + return path.replace(queryMatcher, ''); + }, + + /** + * Returns a version of the given path with the parameters in the given + * query merged into the query string. + */ + withQuery: function withQuery(path, query) { + var existingQuery = PathUtils.extractQuery(path); + + if (existingQuery) query = query ? assign(existingQuery, query) : existingQuery; + + var queryString = qs.stringify(query, { arrayFormat: 'brackets' }); + + if (queryString) { + return PathUtils.withoutQuery(path) + '?' + queryString; + }return PathUtils.withoutQuery(path); + } + +}; + +module.exports = PathUtils; +},{"object-assign":36,"qs":37,"react/lib/invariant":191}],9:[function(require,module,exports){ +'use strict'; + +var assign = require('react/lib/Object.assign'); +var ReactPropTypes = require('react').PropTypes; +var Route = require('./Route'); + +var PropTypes = assign({}, ReactPropTypes, { + + /** + * Indicates that a prop should be falsy. + */ + falsy: function falsy(props, propName, componentName) { + if (props[propName]) { + return new Error('<' + componentName + '> should not have a "' + propName + '" prop'); + } + }, + + /** + * Indicates that a prop should be a Route object. + */ + route: ReactPropTypes.instanceOf(Route), + + /** + * Indicates that a prop should be a Router object. + */ + //router: ReactPropTypes.instanceOf(Router) // TODO + router: ReactPropTypes.func + +}); + +module.exports = PropTypes; +},{"./Route":11,"react":"react","react/lib/Object.assign":69}],10:[function(require,module,exports){ +/** + * Encapsulates a redirect to the given route. + */ +"use strict"; + +function Redirect(to, params, query) { + this.to = to; + this.params = params; + this.query = query; +} + +module.exports = Redirect; +},{}],11:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var assign = require('react/lib/Object.assign'); +var invariant = require('react/lib/invariant'); +var warning = require('react/lib/warning'); +var PathUtils = require('./PathUtils'); + +var _currentRoute; + +var Route = (function () { + function Route(name, path, ignoreScrollBehavior, isDefault, isNotFound, onEnter, onLeave, handler) { + _classCallCheck(this, Route); + + this.name = name; + this.path = path; + this.paramNames = PathUtils.extractParamNames(this.path); + this.ignoreScrollBehavior = !!ignoreScrollBehavior; + this.isDefault = !!isDefault; + this.isNotFound = !!isNotFound; + this.onEnter = onEnter; + this.onLeave = onLeave; + this.handler = handler; + } + + _createClass(Route, [{ + key: 'appendChild', + + /** + * Appends the given route to this route's child routes. + */ + value: function appendChild(route) { + invariant(route instanceof Route, 'route.appendChild must use a valid Route'); + + if (!this.childRoutes) this.childRoutes = []; + + this.childRoutes.push(route); + } + }, { + key: 'toString', + value: function toString() { + var string = ''; + + return string; + } + }], [{ + key: 'createRoute', + + /** + * Creates and returns a new route. Options may be a URL pathname string + * with placeholders for named params or an object with any of the following + * properties: + * + * - name The name of the route. This is used to lookup a + * route relative to its parent route and should be + * unique among all child routes of the same parent + * - path A URL pathname string with optional placeholders + * that specify the names of params to extract from + * the URL when the path matches. Defaults to `/${name}` + * when there is a name given, or the path of the parent + * route, or / + * - ignoreScrollBehavior True to make this route (and all descendants) ignore + * the scroll behavior of the router + * - isDefault True to make this route the default route among all + * its siblings + * - isNotFound True to make this route the "not found" route among + * all its siblings + * - onEnter A transition hook that will be called when the + * router is going to enter this route + * - onLeave A transition hook that will be called when the + * router is going to leave this route + * - handler A React component that will be rendered when + * this route is active + * - parentRoute The parent route to use for this route. This option + * is automatically supplied when creating routes inside + * the callback to another invocation of createRoute. You + * only ever need to use this when declaring routes + * independently of one another to manually piece together + * the route hierarchy + * + * The callback may be used to structure your route hierarchy. Any call to + * createRoute, createDefaultRoute, createNotFoundRoute, or createRedirect + * inside the callback automatically uses this route as its parent. + */ + value: function createRoute(options, callback) { + options = options || {}; + + if (typeof options === 'string') options = { path: options }; + + var parentRoute = _currentRoute; + + if (parentRoute) { + warning(options.parentRoute == null || options.parentRoute === parentRoute, 'You should not use parentRoute with createRoute inside another route\'s child callback; it is ignored'); + } else { + parentRoute = options.parentRoute; + } + + var name = options.name; + var path = options.path || name; + + if (path && !(options.isDefault || options.isNotFound)) { + if (PathUtils.isAbsolute(path)) { + if (parentRoute) { + invariant(path === parentRoute.path || parentRoute.paramNames.length === 0, 'You cannot nest path "%s" inside "%s"; the parent requires URL parameters', path, parentRoute.path); + } + } else if (parentRoute) { + // Relative paths extend their parent. + path = PathUtils.join(parentRoute.path, path); + } else { + path = '/' + path; + } + } else { + path = parentRoute ? parentRoute.path : '/'; + } + + if (options.isNotFound && !/\*$/.test(path)) path += '*'; // Auto-append * to the path of not found routes. + + var route = new Route(name, path, options.ignoreScrollBehavior, options.isDefault, options.isNotFound, options.onEnter, options.onLeave, options.handler); + + if (parentRoute) { + if (route.isDefault) { + invariant(parentRoute.defaultRoute == null, '%s may not have more than one default route', parentRoute); + + parentRoute.defaultRoute = route; + } else if (route.isNotFound) { + invariant(parentRoute.notFoundRoute == null, '%s may not have more than one not found route', parentRoute); + + parentRoute.notFoundRoute = route; + } + + parentRoute.appendChild(route); + } + + // Any routes created in the callback + // use this route as their parent. + if (typeof callback === 'function') { + var currentRoute = _currentRoute; + _currentRoute = route; + callback.call(route, route); + _currentRoute = currentRoute; + } + + return route; + } + }, { + key: 'createDefaultRoute', + + /** + * Creates and returns a route that is rendered when its parent matches + * the current URL. + */ + value: function createDefaultRoute(options) { + return Route.createRoute(assign({}, options, { isDefault: true })); + } + }, { + key: 'createNotFoundRoute', + + /** + * Creates and returns a route that is rendered when its parent matches + * the current URL but none of its siblings do. + */ + value: function createNotFoundRoute(options) { + return Route.createRoute(assign({}, options, { isNotFound: true })); + } + }, { + key: 'createRedirect', + + /** + * Creates and returns a route that automatically redirects the transition + * to another route. In addition to the normal options to createRoute, this + * function accepts the following options: + * + * - from An alias for the `path` option. Defaults to * + * - to The path/route/route name to redirect to + * - params The params to use in the redirect URL. Defaults + * to using the current params + * - query The query to use in the redirect URL. Defaults + * to using the current query + */ + value: function createRedirect(options) { + return Route.createRoute(assign({}, options, { + path: options.path || options.from || '*', + onEnter: function onEnter(transition, params, query) { + transition.redirect(options.to, options.params || params, options.query || query); + } + })); + } + }]); + + return Route; +})(); + +module.exports = Route; +},{"./PathUtils":8,"react/lib/Object.assign":69,"react/lib/invariant":191,"react/lib/warning":212}],12:[function(require,module,exports){ +'use strict'; + +var invariant = require('react/lib/invariant'); +var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; +var getWindowScrollPosition = require('./getWindowScrollPosition'); + +function shouldUpdateScroll(state, prevState) { + if (!prevState) { + return true; + } // Don't update scroll position when only the query has changed. + if (state.pathname === prevState.pathname) { + return false; + }var routes = state.routes; + var prevRoutes = prevState.routes; + + var sharedAncestorRoutes = routes.filter(function (route) { + return prevRoutes.indexOf(route) !== -1; + }); + + return !sharedAncestorRoutes.some(function (route) { + return route.ignoreScrollBehavior; + }); +} + +/** + * Provides the router with the ability to manage window scroll position + * according to its scroll behavior. + */ +var ScrollHistory = { + + statics: { + + /** + * Records curent scroll position as the last known position for the given URL path. + */ + recordScrollPosition: function recordScrollPosition(path) { + if (!this.scrollHistory) this.scrollHistory = {}; + + this.scrollHistory[path] = getWindowScrollPosition(); + }, + + /** + * Returns the last known scroll position for the given URL path. + */ + getScrollPosition: function getScrollPosition(path) { + if (!this.scrollHistory) this.scrollHistory = {}; + + return this.scrollHistory[path] || null; + } + + }, + + componentWillMount: function componentWillMount() { + invariant(this.constructor.getScrollBehavior() == null || canUseDOM, 'Cannot use scroll behavior without a DOM'); + }, + + componentDidMount: function componentDidMount() { + this._updateScroll(); + }, + + componentDidUpdate: function componentDidUpdate(prevProps, prevState) { + this._updateScroll(prevState); + }, + + _updateScroll: function _updateScroll(prevState) { + if (!shouldUpdateScroll(this.state, prevState)) { + return; + }var scrollBehavior = this.constructor.getScrollBehavior(); + + if (scrollBehavior) scrollBehavior.updateScrollPosition(this.constructor.getScrollPosition(this.state.path), this.state.action); + } + +}; + +module.exports = ScrollHistory; +},{"./getWindowScrollPosition":27,"react/lib/ExecutionEnvironment":62,"react/lib/invariant":191}],13:[function(require,module,exports){ +'use strict'; + +var PropTypes = require('./PropTypes'); + +/** + * A mixin for components that need to know the path, routes, URL + * params and query that are currently active. + * + * Example: + * + * var AboutLink = React.createClass({ + * mixins: [ Router.State ], + * render() { + * var className = this.props.className; + * + * if (this.isActive('about')) + * className += ' is-active'; + * + * return React.DOM.a({ className: className }, this.props.children); + * } + * }); + */ +var State = { + + contextTypes: { + router: PropTypes.router.isRequired + }, + + /** + * Returns the current URL path. + */ + getPath: function getPath() { + return this.context.router.getCurrentPath(); + }, + + /** + * Returns the current URL path without the query string. + */ + getPathname: function getPathname() { + return this.context.router.getCurrentPathname(); + }, + + /** + * Returns an object of the URL params that are currently active. + */ + getParams: function getParams() { + return this.context.router.getCurrentParams(); + }, + + /** + * Returns an object of the query params that are currently active. + */ + getQuery: function getQuery() { + return this.context.router.getCurrentQuery(); + }, + + /** + * Returns an array of the routes that are currently active. + */ + getRoutes: function getRoutes() { + return this.context.router.getCurrentRoutes(); + }, + + /** + * A helper method to determine if a given route, params, and query + * are active. + */ + isActive: function isActive(to, params, query) { + return this.context.router.isActive(to, params, query); + } + +}; + +module.exports = State; +},{"./PropTypes":9}],14:[function(require,module,exports){ +/* jshint -W058 */ + +'use strict'; + +var Cancellation = require('./Cancellation'); +var Redirect = require('./Redirect'); + +/** + * Encapsulates a transition to a given path. + * + * The willTransitionTo and willTransitionFrom handlers receive + * an instance of this class as their first argument. + */ +function Transition(path, retry) { + this.path = path; + this.abortReason = null; + // TODO: Change this to router.retryTransition(transition) + this.retry = retry.bind(this); +} + +Transition.prototype.abort = function (reason) { + if (this.abortReason == null) this.abortReason = reason || 'ABORT'; +}; + +Transition.prototype.redirect = function (to, params, query) { + this.abort(new Redirect(to, params, query)); +}; + +Transition.prototype.cancel = function () { + this.abort(new Cancellation()); +}; + +Transition.from = function (transition, routes, components, callback) { + routes.reduce(function (callback, route, index) { + return function (error) { + if (error || transition.abortReason) { + callback(error); + } else if (route.onLeave) { + try { + route.onLeave(transition, components[index], callback); + + // If there is no callback in the argument list, call it automatically. + if (route.onLeave.length < 3) callback(); + } catch (e) { + callback(e); + } + } else { + callback(); + } + }; + }, callback)(); +}; + +Transition.to = function (transition, routes, params, query, callback) { + routes.reduceRight(function (callback, route) { + return function (error) { + if (error || transition.abortReason) { + callback(error); + } else if (route.onEnter) { + try { + route.onEnter(transition, params, query, callback); + + // If there is no callback in the argument list, call it automatically. + if (route.onEnter.length < 4) callback(); + } catch (e) { + callback(e); + } + } else { + callback(); + } + }; + }, callback)(); +}; + +module.exports = Transition; +},{"./Cancellation":4,"./Redirect":10}],15:[function(require,module,exports){ +/** + * Actions that modify the URL. + */ +'use strict'; + +var LocationActions = { + + /** + * Indicates a new location is being pushed to the history stack. + */ + PUSH: 'push', + + /** + * Indicates the current location should be replaced. + */ + REPLACE: 'replace', + + /** + * Indicates the most recent entry should be removed from the history stack. + */ + POP: 'pop' + +}; + +module.exports = LocationActions; +},{}],16:[function(require,module,exports){ +'use strict'; + +var LocationActions = require('../actions/LocationActions'); + +/** + * A scroll behavior that attempts to imitate the default behavior + * of modern browsers. + */ +var ImitateBrowserBehavior = { + + updateScrollPosition: function updateScrollPosition(position, actionType) { + switch (actionType) { + case LocationActions.PUSH: + case LocationActions.REPLACE: + window.scrollTo(0, 0); + break; + case LocationActions.POP: + if (position) { + window.scrollTo(position.x, position.y); + } else { + window.scrollTo(0, 0); + } + break; + } + } + +}; + +module.exports = ImitateBrowserBehavior; +},{"../actions/LocationActions":15}],17:[function(require,module,exports){ +/** + * A scroll behavior that always scrolls to the top of the page + * after a transition. + */ +"use strict"; + +var ScrollToTopBehavior = { + + updateScrollPosition: function updateScrollPosition() { + window.scrollTo(0, 0); + } + +}; + +module.exports = ScrollToTopBehavior; +},{}],18:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +/** + * This component is necessary to get around a context warning + * present in React 0.13.0. It sovles this by providing a separation + * between the "owner" and "parent" contexts. + */ + +var React = require('react'); + +var ContextWrapper = (function (_React$Component) { + function ContextWrapper() { + _classCallCheck(this, ContextWrapper); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(ContextWrapper, _React$Component); + + _createClass(ContextWrapper, [{ + key: 'render', + value: function render() { + return this.props.children; + } + }]); + + return ContextWrapper; +})(React.Component); + +module.exports = ContextWrapper; +},{"react":"react"}],19:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var PropTypes = require('../PropTypes'); +var RouteHandler = require('./RouteHandler'); +var Route = require('./Route'); + +/** + * A component is a special kind of that + * renders when its parent matches but none of its siblings do. + * Only one such route may be used at any given level in the + * route hierarchy. + */ + +var DefaultRoute = (function (_Route) { + function DefaultRoute() { + _classCallCheck(this, DefaultRoute); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(DefaultRoute, _Route); + + return DefaultRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +DefaultRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +DefaultRoute.defaultProps = { + handler: RouteHandler +}; + +module.exports = DefaultRoute; +},{"../PropTypes":9,"./Route":23,"./RouteHandler":24}],20:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var React = require('react'); +var assign = require('react/lib/Object.assign'); +var PropTypes = require('../PropTypes'); + +function isLeftClickEvent(event) { + return event.button === 0; +} + +function isModifiedEvent(event) { + return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); +} + +/** + * components are used to create an element that links to a route. + * When that route is active, the link gets an "active" class name (or the + * value of its `activeClassName` prop). + * + * For example, assuming you have the following route: + * + * + * + * You could use the following component to link to that route: + * + * + * + * In addition to params, links may pass along query string parameters + * using the `query` prop. + * + * + */ + +var Link = (function (_React$Component) { + function Link() { + _classCallCheck(this, Link); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(Link, _React$Component); + + _createClass(Link, [{ + key: 'handleClick', + value: function handleClick(event) { + var allowTransition = true; + var clickResult; + + if (this.props.onClick) clickResult = this.props.onClick(event); + + if (isModifiedEvent(event) || !isLeftClickEvent(event)) { + return; + }if (clickResult === false || event.defaultPrevented === true) allowTransition = false; + + event.preventDefault(); + + if (allowTransition) this.context.router.transitionTo(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'getHref', + + /** + * Returns the value of the "href" attribute to use on the DOM element. + */ + value: function getHref() { + return this.context.router.makeHref(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'getClassName', + + /** + * Returns the value of the "class" attribute to use on the DOM element, which contains + * the value of the activeClassName property when this is active. + */ + value: function getClassName() { + var className = this.props.className; + + if (this.getActiveState()) className += ' ' + this.props.activeClassName; + + return className; + } + }, { + key: 'getActiveState', + value: function getActiveState() { + return this.context.router.isActive(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'render', + value: function render() { + var props = assign({}, this.props, { + href: this.getHref(), + className: this.getClassName(), + onClick: this.handleClick.bind(this) + }); + + if (props.activeStyle && this.getActiveState()) props.style = props.activeStyle; + + return React.DOM.a(props, this.props.children); + } + }]); + + return Link; +})(React.Component); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Link.contextTypes = { + router: PropTypes.router.isRequired +}; + +Link.propTypes = { + activeClassName: PropTypes.string.isRequired, + to: PropTypes.oneOfType([PropTypes.string, PropTypes.route]).isRequired, + params: PropTypes.object, + query: PropTypes.object, + activeStyle: PropTypes.object, + onClick: PropTypes.func +}; + +Link.defaultProps = { + activeClassName: 'active', + className: '' +}; + +module.exports = Link; +},{"../PropTypes":9,"react":"react","react/lib/Object.assign":69}],21:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var PropTypes = require('../PropTypes'); +var RouteHandler = require('./RouteHandler'); +var Route = require('./Route'); + +/** + * A is a special kind of that + * renders when the beginning of its parent's path matches + * but none of its siblings do, including any . + * Only one such route may be used at any given level in the + * route hierarchy. + */ + +var NotFoundRoute = (function (_Route) { + function NotFoundRoute() { + _classCallCheck(this, NotFoundRoute); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(NotFoundRoute, _Route); + + return NotFoundRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +NotFoundRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +NotFoundRoute.defaultProps = { + handler: RouteHandler +}; + +module.exports = NotFoundRoute; +},{"../PropTypes":9,"./Route":23,"./RouteHandler":24}],22:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var PropTypes = require('../PropTypes'); +var Route = require('./Route'); + +/** + * A component is a special kind of that always + * redirects to another route when it matches. + */ + +var Redirect = (function (_Route) { + function Redirect() { + _classCallCheck(this, Redirect); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(Redirect, _Route); + + return Redirect; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Redirect.propTypes = { + path: PropTypes.string, + from: PropTypes.string, // Alias for path. + to: PropTypes.string, + handler: PropTypes.falsy +}; + +// Redirects should not have a default handler +Redirect.defaultProps = {}; + +module.exports = Redirect; +},{"../PropTypes":9,"./Route":23}],23:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var React = require('react'); +var invariant = require('react/lib/invariant'); +var PropTypes = require('../PropTypes'); +var RouteHandler = require('./RouteHandler'); + +/** + * components specify components that are rendered to the page when the + * URL matches a given pattern. + * + * Routes are arranged in a nested tree structure. When a new URL is requested, + * the tree is searched depth-first to find a route whose path matches the URL. + * When one is found, all routes in the tree that lead to it are considered + * "active" and their components are rendered into the DOM, nested in the same + * order as they are in the tree. + * + * The preferred way to configure a router is using JSX. The XML-like syntax is + * a great way to visualize how routes are laid out in an application. + * + * var routes = [ + * + * + * + * + * + * ]; + * + * Router.run(routes, function (Handler) { + * React.render(, document.body); + * }); + * + * Handlers for Route components that contain children can render their active + * child route using a element. + * + * var App = React.createClass({ + * render: function () { + * return ( + *
    + * + *
    + * ); + * } + * }); + * + * If no handler is provided for the route, it will render a matched child route. + */ + +var Route = (function (_React$Component) { + function Route() { + _classCallCheck(this, Route); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(Route, _React$Component); + + _createClass(Route, [{ + key: 'render', + value: function render() { + invariant(false, '%s elements are for router configuration only and should not be rendered', this.constructor.name); + } + }]); + + return Route; +})(React.Component); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Route.propTypes = { + name: PropTypes.string, + path: PropTypes.string, + handler: PropTypes.func, + ignoreScrollBehavior: PropTypes.bool +}; + +Route.defaultProps = { + handler: RouteHandler +}; + +module.exports = Route; +},{"../PropTypes":9,"./RouteHandler":24,"react":"react","react/lib/invariant":191}],24:[function(require,module,exports){ +'use strict'; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var React = require('react'); +var ContextWrapper = require('./ContextWrapper'); +var assign = require('react/lib/Object.assign'); +var PropTypes = require('../PropTypes'); + +var REF_NAME = '__routeHandler__'; + +/** + * A component renders the active child route handler + * when routes are nested. + */ + +var RouteHandler = (function (_React$Component) { + function RouteHandler() { + _classCallCheck(this, RouteHandler); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(RouteHandler, _React$Component); + + _createClass(RouteHandler, [{ + key: 'getChildContext', + value: function getChildContext() { + return { + routeDepth: this.context.routeDepth + 1 + }; + } + }, { + key: 'componentDidMount', + value: function componentDidMount() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, { + key: 'componentDidUpdate', + value: function componentDidUpdate() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, { + key: 'componentWillUnmount', + value: function componentWillUnmount() { + this._updateRouteComponent(null); + } + }, { + key: '_updateRouteComponent', + value: function _updateRouteComponent(component) { + this.context.router.setRouteComponentAtDepth(this.getRouteDepth(), component); + } + }, { + key: 'getRouteDepth', + value: function getRouteDepth() { + return this.context.routeDepth; + } + }, { + key: 'createChildRouteHandler', + value: function createChildRouteHandler(props) { + var route = this.context.router.getRouteAtDepth(this.getRouteDepth()); + + if (route == null) { + return null; + }var childProps = assign({}, props || this.props, { + ref: REF_NAME, + params: this.context.router.getCurrentParams(), + query: this.context.router.getCurrentQuery() + }); + + return React.createElement(route.handler, childProps); + } + }, { + key: 'render', + value: function render() { + var handler = this.createChildRouteHandler(); + // + + + + + \ No newline at end of file diff --git a/mitmproxy/webfonts/fontawesome-webfont.eot b/mitmproxy/webfonts/fontawesome-webfont.eot new file mode 100644 index 00000000..84677bc0 Binary files /dev/null and b/mitmproxy/webfonts/fontawesome-webfont.eot differ diff --git a/mitmproxy/webfonts/fontawesome-webfont.svg b/mitmproxy/webfonts/fontawesome-webfont.svg new file mode 100644 index 00000000..d907b25a --- /dev/null +++ b/mitmproxy/webfonts/fontawesome-webfont.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mitmproxy/webfonts/fontawesome-webfont.ttf b/mitmproxy/webfonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..96a3639c Binary files /dev/null and b/mitmproxy/webfonts/fontawesome-webfont.ttf differ diff --git a/mitmproxy/webfonts/fontawesome-webfont.woff b/mitmproxy/webfonts/fontawesome-webfont.woff new file mode 100644 index 00000000..628b6a52 Binary files /dev/null and b/mitmproxy/webfonts/fontawesome-webfont.woff differ diff --git a/netlib/README.rst b/netlib/README.rst deleted file mode 100644 index 16bd65a7..00000000 --- a/netlib/README.rst +++ /dev/null @@ -1,35 +0,0 @@ -|travis| |coveralls| |downloads| |latest_release| |python_versions| - -Netlib is a collection of network utility classes, used by the pathod and -mitmproxy projects. It differs from other projects in some fundamental -respects, because both pathod and mitmproxy often need to violate standards. -This means that protocols are implemented as small, well-contained and flexible -functions, and are designed to allow misbehaviour when needed. - - -Development ------------ - -If you'd like to work on netlib, check out the instructions in mitmproxy's README_. - -.. |travis| image:: https://shields.mitmproxy.org/travis/mitmproxy/netlib/master.svg - :target: https://travis-ci.org/mitmproxy/netlib - :alt: Build Status - -.. |coveralls| image:: https://shields.mitmproxy.org/coveralls/mitmproxy/netlib/master.svg - :target: https://coveralls.io/r/mitmproxy/netlib - :alt: Coverage Status - -.. |downloads| image:: https://shields.mitmproxy.org/pypi/dm/netlib.svg?color=orange - :target: https://pypi.python.org/pypi/netlib - :alt: Downloads - -.. |latest_release| image:: https://shields.mitmproxy.org/pypi/v/netlib.svg - :target: https://pypi.python.org/pypi/netlib - :alt: Latest Version - -.. |python_versions| image:: https://shields.mitmproxy.org/pypi/pyversions/netlib.svg - :target: https://pypi.python.org/pypi/netlib - :alt: Supported Python versions - -.. _README: https://github.com/mitmproxy/mitmproxy#hacking \ No newline at end of file diff --git a/netlib/__init__.py b/netlib/__init__.py new file mode 100644 index 00000000..9b4faa33 --- /dev/null +++ b/netlib/__init__.py @@ -0,0 +1 @@ +from __future__ import (absolute_import, print_function, division) diff --git a/netlib/certutils.py b/netlib/certutils.py new file mode 100644 index 00000000..616a778e --- /dev/null +++ b/netlib/certutils.py @@ -0,0 +1,472 @@ +from __future__ import (absolute_import, print_function, division) +import os +import ssl +import time +import datetime +from six.moves import filter +import ipaddress + +import sys +from pyasn1.type import univ, constraint, char, namedtype, tag +from pyasn1.codec.der.decoder import decode +from pyasn1.error import PyAsn1Error +import OpenSSL + +from .utils import Serializable + +# Default expiry must not be too long: https://github.com/mitmproxy/mitmproxy/issues/815 + +DEFAULT_EXP = 94608000 # = 24 * 60 * 60 * 365 * 3 +# Generated with "openssl dhparam". It's too slow to generate this on startup. +DEFAULT_DHPARAM = b""" +-----BEGIN DH PARAMETERS----- +MIICCAKCAgEAyT6LzpwVFS3gryIo29J5icvgxCnCebcdSe/NHMkD8dKJf8suFCg3 +O2+dguLakSVif/t6dhImxInJk230HmfC8q93hdcg/j8rLGJYDKu3ik6H//BAHKIv +j5O9yjU3rXCfmVJQic2Nne39sg3CreAepEts2TvYHhVv3TEAzEqCtOuTjgDv0ntJ +Gwpj+BJBRQGG9NvprX1YGJ7WOFBP/hWU7d6tgvE6Xa7T/u9QIKpYHMIkcN/l3ZFB +chZEqVlyrcngtSXCROTPcDOQ6Q8QzhaBJS+Z6rcsd7X+haiQqvoFcmaJ08Ks6LQC +ZIL2EtYJw8V8z7C0igVEBIADZBI6OTbuuhDwRw//zU1uq52Oc48CIZlGxTYG/Evq +o9EWAXUYVzWkDSTeBH1r4z/qLPE2cnhtMxbFxuvK53jGB0emy2y1Ei6IhKshJ5qX +IB/aE7SSHyQ3MDHHkCmQJCsOd4Mo26YX61NZ+n501XjqpCBQ2+DfZCBh8Va2wDyv +A2Ryg9SUz8j0AXViRNMJgJrr446yro/FuJZwnQcO3WQnXeqSBnURqKjmqkeFP+d8 +6mk2tqJaY507lRNqtGlLnj7f5RNoBFJDCLBNurVgfvq9TCVWKDIFD4vZRjCrnl6I +rD693XKIHUCWOjMh1if6omGXKHH40QuME2gNa50+YPn1iYDl88uDbbMCAQI= +-----END DH PARAMETERS----- +""" + + +def create_ca(o, cn, exp): + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) + cert = OpenSSL.crypto.X509() + cert.set_serial_number(int(time.time() * 10000)) + cert.set_version(2) + cert.get_subject().CN = cn + cert.get_subject().O = o + cert.gmtime_adj_notBefore(-3600 * 48) + cert.gmtime_adj_notAfter(exp) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(key) + cert.add_extensions([ + OpenSSL.crypto.X509Extension( + b"basicConstraints", + True, + b"CA:TRUE" + ), + OpenSSL.crypto.X509Extension( + b"nsCertType", + False, + b"sslCA" + ), + OpenSSL.crypto.X509Extension( + b"extendedKeyUsage", + False, + b"serverAuth,clientAuth,emailProtection,timeStamping,msCodeInd,msCodeCom,msCTLSign,msSGC,msEFS,nsSGC" + ), + OpenSSL.crypto.X509Extension( + b"keyUsage", + True, + b"keyCertSign, cRLSign" + ), + OpenSSL.crypto.X509Extension( + b"subjectKeyIdentifier", + False, + b"hash", + subject=cert + ), + ]) + cert.sign(key, "sha256") + return key, cert + + +def dummy_cert(privkey, cacert, commonname, sans): + """ + Generates a dummy certificate. + + privkey: CA private key + cacert: CA certificate + commonname: Common name for the generated certificate. + sans: A list of Subject Alternate Names. + + Returns cert if operation succeeded, None if not. + """ + ss = [] + for i in sans: + try: + ipaddress.ip_address(i.decode("ascii")) + except ValueError: + ss.append(b"DNS: %s" % i) + else: + ss.append(b"IP: %s" % i) + ss = b", ".join(ss) + + cert = OpenSSL.crypto.X509() + cert.gmtime_adj_notBefore(-3600 * 48) + cert.gmtime_adj_notAfter(DEFAULT_EXP) + cert.set_issuer(cacert.get_subject()) + if commonname is not None: + cert.get_subject().CN = commonname + cert.set_serial_number(int(time.time() * 10000)) + if ss: + cert.set_version(2) + cert.add_extensions( + [OpenSSL.crypto.X509Extension(b"subjectAltName", False, ss)]) + cert.set_pubkey(cacert.get_pubkey()) + cert.sign(privkey, "sha256") + return SSLCert(cert) + + +# DNTree did not pass TestCertStore.test_sans_change and is temporarily replaced by a simple dict. +# +# class _Node(UserDict.UserDict): +# def __init__(self): +# UserDict.UserDict.__init__(self) +# self.value = None +# +# +# class DNTree: +# """ +# Domain store that knows about wildcards. DNS wildcards are very +# restricted - the only valid variety is an asterisk on the left-most +# domain component, i.e.: +# +# *.foo.com +# """ +# def __init__(self): +# self.d = _Node() +# +# def add(self, dn, cert): +# parts = dn.split(".") +# parts.reverse() +# current = self.d +# for i in parts: +# current = current.setdefault(i, _Node()) +# current.value = cert +# +# def get(self, dn): +# parts = dn.split(".") +# current = self.d +# for i in reversed(parts): +# if i in current: +# current = current[i] +# elif "*" in current: +# return current["*"].value +# else: +# return None +# return current.value + + +class CertStoreEntry(object): + + def __init__(self, cert, privatekey, chain_file): + self.cert = cert + self.privatekey = privatekey + self.chain_file = chain_file + + +class CertStore(object): + + """ + Implements an in-memory certificate store. + """ + + def __init__( + self, + default_privatekey, + default_ca, + default_chain_file, + dhparams): + self.default_privatekey = default_privatekey + self.default_ca = default_ca + self.default_chain_file = default_chain_file + self.dhparams = dhparams + self.certs = dict() + + @staticmethod + def load_dhparam(path): + + # netlib<=0.10 doesn't generate a dhparam file. + # Create it now if neccessary. + if not os.path.exists(path): + with open(path, "wb") as f: + f.write(DEFAULT_DHPARAM) + + bio = OpenSSL.SSL._lib.BIO_new_file(path.encode(sys.getfilesystemencoding()), b"r") + if bio != OpenSSL.SSL._ffi.NULL: + bio = OpenSSL.SSL._ffi.gc(bio, OpenSSL.SSL._lib.BIO_free) + dh = OpenSSL.SSL._lib.PEM_read_bio_DHparams( + bio, + OpenSSL.SSL._ffi.NULL, + OpenSSL.SSL._ffi.NULL, + OpenSSL.SSL._ffi.NULL) + dh = OpenSSL.SSL._ffi.gc(dh, OpenSSL.SSL._lib.DH_free) + return dh + + @classmethod + def from_store(cls, path, basename): + ca_path = os.path.join(path, basename + "-ca.pem") + if not os.path.exists(ca_path): + key, ca = cls.create_store(path, basename) + else: + with open(ca_path, "rb") as f: + raw = f.read() + ca = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, + raw) + key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + raw) + dh_path = os.path.join(path, basename + "-dhparam.pem") + dh = cls.load_dhparam(dh_path) + return cls(key, ca, ca_path, dh) + + @staticmethod + def create_store(path, basename, o=None, cn=None, expiry=DEFAULT_EXP): + if not os.path.exists(path): + os.makedirs(path) + + o = o or basename + cn = cn or basename + + key, ca = create_ca(o=o, cn=cn, exp=expiry) + # Dump the CA plus private key + with open(os.path.join(path, basename + "-ca.pem"), "wb") as f: + f.write( + OpenSSL.crypto.dump_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + key)) + f.write( + OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, + ca)) + + # Dump the certificate in PEM format + with open(os.path.join(path, basename + "-ca-cert.pem"), "wb") as f: + f.write( + OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, + ca)) + + # Create a .cer file with the same contents for Android + with open(os.path.join(path, basename + "-ca-cert.cer"), "wb") as f: + f.write( + OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, + ca)) + + # Dump the certificate in PKCS12 format for Windows devices + with open(os.path.join(path, basename + "-ca-cert.p12"), "wb") as f: + p12 = OpenSSL.crypto.PKCS12() + p12.set_certificate(ca) + p12.set_privatekey(key) + f.write(p12.export()) + + with open(os.path.join(path, basename + "-dhparam.pem"), "wb") as f: + f.write(DEFAULT_DHPARAM) + + return key, ca + + def add_cert_file(self, spec, path): + with open(path, "rb") as f: + raw = f.read() + cert = SSLCert( + OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, + raw)) + try: + privatekey = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + raw) + except Exception: + privatekey = self.default_privatekey + self.add_cert( + CertStoreEntry(cert, privatekey, path), + spec + ) + + def add_cert(self, entry, *names): + """ + Adds a cert to the certstore. We register the CN in the cert plus + any SANs, and also the list of names provided as an argument. + """ + if entry.cert.cn: + self.certs[entry.cert.cn] = entry + for i in entry.cert.altnames: + self.certs[i] = entry + for i in names: + self.certs[i] = entry + + @staticmethod + def asterisk_forms(dn): + if dn is None: + return [] + parts = dn.split(b".") + parts.reverse() + curr_dn = b"" + dn_forms = [b"*"] + for part in parts[:-1]: + curr_dn = b"." + part + curr_dn # .example.com + dn_forms.append(b"*" + curr_dn) # *.example.com + if parts[-1] != b"*": + dn_forms.append(parts[-1] + curr_dn) + return dn_forms + + def get_cert(self, commonname, sans): + """ + Returns an (cert, privkey, cert_chain) tuple. + + commonname: Common name for the generated certificate. Must be a + valid, plain-ASCII, IDNA-encoded domain name. + + sans: A list of Subject Alternate Names. + """ + + potential_keys = self.asterisk_forms(commonname) + for s in sans: + potential_keys.extend(self.asterisk_forms(s)) + potential_keys.append((commonname, tuple(sans))) + + name = next( + filter(lambda key: key in self.certs, potential_keys), + None + ) + if name: + entry = self.certs[name] + else: + entry = CertStoreEntry( + cert=dummy_cert( + self.default_privatekey, + self.default_ca, + commonname, + sans), + privatekey=self.default_privatekey, + chain_file=self.default_chain_file) + self.certs[(commonname, tuple(sans))] = entry + + return entry.cert, entry.privatekey, entry.chain_file + + +class _GeneralName(univ.Choice): + # We are only interested in dNSNames. We use a default handler to ignore + # other types. + # TODO: We should also handle iPAddresses. + componentType = namedtype.NamedTypes( + namedtype.NamedType('dNSName', char.IA5String().subtype( + implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2) + ) + ), + ) + + +class _GeneralNames(univ.SequenceOf): + componentType = _GeneralName() + sizeSpec = univ.SequenceOf.sizeSpec + \ + constraint.ValueSizeConstraint(1, 1024) + + +class SSLCert(Serializable): + + def __init__(self, cert): + """ + Returns a (common name, [subject alternative names]) tuple. + """ + self.x509 = cert + + def __eq__(self, other): + return self.digest("sha256") == other.digest("sha256") + + def __ne__(self, other): + return not self.__eq__(other) + + def get_state(self): + return self.to_pem() + + def set_state(self, state): + self.x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, state) + + @classmethod + def from_state(cls, state): + cls.from_pem(state) + + @classmethod + def from_pem(cls, txt): + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, txt) + return cls(x509) + + @classmethod + def from_der(cls, der): + pem = ssl.DER_cert_to_PEM_cert(der) + return cls.from_pem(pem) + + def to_pem(self): + return OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, + self.x509) + + def digest(self, name): + return self.x509.digest(name) + + @property + def issuer(self): + return self.x509.get_issuer().get_components() + + @property + def notbefore(self): + t = self.x509.get_notBefore() + return datetime.datetime.strptime(t.decode("ascii"), "%Y%m%d%H%M%SZ") + + @property + def notafter(self): + t = self.x509.get_notAfter() + return datetime.datetime.strptime(t.decode("ascii"), "%Y%m%d%H%M%SZ") + + @property + def has_expired(self): + return self.x509.has_expired() + + @property + def subject(self): + return self.x509.get_subject().get_components() + + @property + def serial(self): + return self.x509.get_serial_number() + + @property + def keyinfo(self): + pk = self.x509.get_pubkey() + types = { + OpenSSL.crypto.TYPE_RSA: "RSA", + OpenSSL.crypto.TYPE_DSA: "DSA", + } + return ( + types.get(pk.type(), "UNKNOWN"), + pk.bits() + ) + + @property + def cn(self): + c = None + for i in self.subject: + if i[0] == b"CN": + c = i[1] + return c + + @property + def altnames(self): + """ + Returns: + All DNS altnames. + """ + # tcp.TCPClient.convert_to_ssl assumes that this property only contains DNS altnames for hostname verification. + altnames = [] + for i in range(self.x509.get_extension_count()): + ext = self.x509.get_extension(i) + if ext.get_short_name() == b"subjectAltName": + try: + dec = decode(ext.get_data(), asn1Spec=_GeneralNames()) + except PyAsn1Error: + continue + for i in dec[0]: + altnames.append(i[0].asOctets()) + return altnames diff --git a/netlib/encoding.py b/netlib/encoding.py new file mode 100644 index 00000000..14479e00 --- /dev/null +++ b/netlib/encoding.py @@ -0,0 +1,88 @@ +""" + Utility functions for decoding response bodies. +""" +from __future__ import absolute_import +from io import BytesIO +import gzip +import zlib +from .utils import always_byte_args + + +ENCODINGS = {"identity", "gzip", "deflate"} + + +def decode(e, content): + if not isinstance(content, bytes): + return None + encoding_map = { + "identity": identity, + "gzip": decode_gzip, + "deflate": decode_deflate, + } + if e not in encoding_map: + return None + return encoding_map[e](content) + + +def encode(e, content): + if not isinstance(content, bytes): + return None + encoding_map = { + "identity": identity, + "gzip": encode_gzip, + "deflate": encode_deflate, + } + if e not in encoding_map: + return None + return encoding_map[e](content) + + +def identity(content): + """ + Returns content unchanged. Identity is the default value of + Accept-Encoding headers. + """ + return content + + +def decode_gzip(content): + gfile = gzip.GzipFile(fileobj=BytesIO(content)) + try: + return gfile.read() + except (IOError, EOFError): + return None + + +def encode_gzip(content): + s = BytesIO() + gf = gzip.GzipFile(fileobj=s, mode='wb') + gf.write(content) + gf.close() + return s.getvalue() + + +def decode_deflate(content): + """ + Returns decompressed data for DEFLATE. Some servers may respond with + compressed data without a zlib header or checksum. An undocumented + feature of zlib permits the lenient decompression of data missing both + values. + + http://bugs.python.org/issue5784 + """ + try: + try: + return zlib.decompress(content) + except zlib.error: + return zlib.decompress(content, -15) + except zlib.error: + return None + + +def encode_deflate(content): + """ + Returns compressed content, always including zlib header and checksum. + """ + return zlib.compress(content) + +__all__ = ["ENCODINGS", "encode", "decode"] diff --git a/netlib/exceptions.py b/netlib/exceptions.py new file mode 100644 index 00000000..05f1054b --- /dev/null +++ b/netlib/exceptions.py @@ -0,0 +1,56 @@ +""" +We try to be very hygienic regarding the exceptions we throw: +Every Exception netlib raises shall be a subclass of NetlibException. + + +See also: http://lucumr.pocoo.org/2014/10/16/on-error-handling/ +""" +from __future__ import absolute_import, print_function, division + + +class NetlibException(Exception): + """ + Base class for all exceptions thrown by netlib. + """ + def __init__(self, message=None): + super(NetlibException, self).__init__(message) + + +class Disconnect(object): + """Immediate EOF""" + + +class HttpException(NetlibException): + pass + + +class HttpReadDisconnect(HttpException, Disconnect): + pass + + +class HttpSyntaxException(HttpException): + pass + + +class TcpException(NetlibException): + pass + + +class TcpDisconnect(TcpException, Disconnect): + pass + + +class TcpReadIncomplete(TcpException): + pass + + +class TcpTimeout(TcpException): + pass + + +class TlsException(NetlibException): + pass + + +class InvalidCertificateException(TlsException): + pass diff --git a/netlib/http/__init__.py b/netlib/http/__init__.py new file mode 100644 index 00000000..fd632cd5 --- /dev/null +++ b/netlib/http/__init__.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import, print_function, division +from .request import Request +from .response import Response +from .headers import Headers +from .message import decoded, CONTENT_MISSING +from . import http1, http2 + +__all__ = [ + "Request", + "Response", + "Headers", + "decoded", "CONTENT_MISSING", + "http1", "http2", +] diff --git a/netlib/http/authentication.py b/netlib/http/authentication.py new file mode 100644 index 00000000..d769abe5 --- /dev/null +++ b/netlib/http/authentication.py @@ -0,0 +1,167 @@ +from __future__ import (absolute_import, print_function, division) +from argparse import Action, ArgumentTypeError +import binascii + + +def parse_http_basic_auth(s): + words = s.split() + if len(words) != 2: + return None + scheme = words[0] + try: + user = binascii.a2b_base64(words[1]).decode("utf8", "replace") + except binascii.Error: + return None + parts = user.split(':') + if len(parts) != 2: + return None + return scheme, parts[0], parts[1] + + +def assemble_http_basic_auth(scheme, username, password): + v = binascii.b2a_base64((username + ":" + password).encode("utf8")).decode("ascii") + return scheme + " " + v + + +class NullProxyAuth(object): + + """ + No proxy auth at all (returns empty challange headers) + """ + + def __init__(self, password_manager): + self.password_manager = password_manager + + def clean(self, headers_): + """ + Clean up authentication headers, so they're not passed upstream. + """ + + def authenticate(self, headers_): + """ + Tests that the user is allowed to use the proxy + """ + return True + + def auth_challenge_headers(self): + """ + Returns a dictionary containing the headers require to challenge the user + """ + return {} + + +class BasicProxyAuth(NullProxyAuth): + CHALLENGE_HEADER = 'Proxy-Authenticate' + AUTH_HEADER = 'Proxy-Authorization' + + def __init__(self, password_manager, realm): + NullProxyAuth.__init__(self, password_manager) + self.realm = realm + + def clean(self, headers): + del headers[self.AUTH_HEADER] + + def authenticate(self, headers): + auth_value = headers.get(self.AUTH_HEADER) + if not auth_value: + return False + parts = parse_http_basic_auth(auth_value) + if not parts: + return False + scheme, username, password = parts + if scheme.lower() != 'basic': + return False + if not self.password_manager.test(username, password): + return False + self.username = username + return True + + def auth_challenge_headers(self): + return {self.CHALLENGE_HEADER: 'Basic realm="%s"' % self.realm} + + +class PassMan(object): + + def test(self, username_, password_token_): + return False + + +class PassManNonAnon(PassMan): + + """ + Ensure the user specifies a username, accept any password. + """ + + def test(self, username, password_token_): + if username: + return True + return False + + +class PassManHtpasswd(PassMan): + + """ + Read usernames and passwords from an htpasswd file + """ + + def __init__(self, path): + """ + Raises ValueError if htpasswd file is invalid. + """ + import passlib.apache + self.htpasswd = passlib.apache.HtpasswdFile(path) + + def test(self, username, password_token): + return bool(self.htpasswd.check_password(username, password_token)) + + +class PassManSingleUser(PassMan): + + def __init__(self, username, password): + self.username, self.password = username, password + + def test(self, username, password_token): + return self.username == username and self.password == password_token + + +class AuthAction(Action): + + """ + Helper class to allow seamless integration int argparse. Example usage: + parser.add_argument( + "--nonanonymous", + action=NonanonymousAuthAction, nargs=0, + help="Allow access to any user long as a credentials are specified." + ) + """ + + def __call__(self, parser, namespace, values, option_string=None): + passman = self.getPasswordManager(values) + authenticator = BasicProxyAuth(passman, "mitmproxy") + setattr(namespace, self.dest, authenticator) + + def getPasswordManager(self, s): # pragma: nocover + raise NotImplementedError() + + +class SingleuserAuthAction(AuthAction): + + def getPasswordManager(self, s): + if len(s.split(':')) != 2: + raise ArgumentTypeError( + "Invalid single-user specification. Please use the format username:password" + ) + username, password = s.split(':') + return PassManSingleUser(username, password) + + +class NonanonymousAuthAction(AuthAction): + + def getPasswordManager(self, s): + return PassManNonAnon() + + +class HtpasswdAuthAction(AuthAction): + + def getPasswordManager(self, s): + return PassManHtpasswd(s) diff --git a/netlib/http/cookies.py b/netlib/http/cookies.py new file mode 100644 index 00000000..18544b5e --- /dev/null +++ b/netlib/http/cookies.py @@ -0,0 +1,193 @@ +import re + +from .. import odict + +""" +A flexible module for cookie parsing and manipulation. + +This module differs from usual standards-compliant cookie modules in a number +of ways. We try to be as permissive as possible, and to retain even mal-formed +information. Duplicate cookies are preserved in parsing, and can be set in +formatting. We do attempt to escape and quote values where needed, but will not +reject data that violate the specs. + +Parsing accepts the formats in RFC6265 and partially RFC2109 and RFC2965. We do +not parse the comma-separated variant of Set-Cookie that allows multiple +cookies to be set in a single header. Technically this should be feasible, but +it turns out that violations of RFC6265 that makes the parsing problem +indeterminate are much more common than genuine occurences of the multi-cookie +variants. Serialization follows RFC6265. + + http://tools.ietf.org/html/rfc6265 + http://tools.ietf.org/html/rfc2109 + http://tools.ietf.org/html/rfc2965 +""" + +# TODO: Disallow LHS-only Cookie values + + +def _read_until(s, start, term): + """ + Read until one of the characters in term is reached. + """ + if start == len(s): + return "", start + 1 + for i in range(start, len(s)): + if s[i] in term: + return s[start:i], i + return s[start:i + 1], i + 1 + + +def _read_token(s, start): + """ + Read a token - the LHS of a token/value pair in a cookie. + """ + return _read_until(s, start, ";=") + + +def _read_quoted_string(s, start): + """ + start: offset to the first quote of the string to be read + + A sort of loose super-set of the various quoted string specifications. + + RFC6265 disallows backslashes or double quotes within quoted strings. + Prior RFCs use backslashes to escape. This leaves us free to apply + backslash escaping by default and be compatible with everything. + """ + escaping = False + ret = [] + # Skip the first quote + i = start # initialize in case the loop doesn't run. + for i in range(start + 1, len(s)): + if escaping: + ret.append(s[i]) + escaping = False + elif s[i] == '"': + break + elif s[i] == "\\": + escaping = True + else: + ret.append(s[i]) + return "".join(ret), i + 1 + + +def _read_value(s, start, delims): + """ + Reads a value - the RHS of a token/value pair in a cookie. + + special: If the value is special, commas are premitted. Else comma + terminates. This helps us support old and new style values. + """ + if start >= len(s): + return "", start + elif s[start] == '"': + return _read_quoted_string(s, start) + else: + return _read_until(s, start, delims) + + +def _read_pairs(s, off=0): + """ + Read pairs of lhs=rhs values. + + off: start offset + specials: a lower-cased list of keys that may contain commas + """ + vals = [] + while True: + lhs, off = _read_token(s, off) + lhs = lhs.lstrip() + if lhs: + rhs = None + if off < len(s): + if s[off] == "=": + rhs, off = _read_value(s, off + 1, ";") + vals.append([lhs, rhs]) + off += 1 + if not off < len(s): + break + return vals, off + + +def _has_special(s): + for i in s: + if i in '",;\\': + return True + o = ord(i) + if o < 0x21 or o > 0x7e: + return True + return False + + +ESCAPE = re.compile(r"([\"\\])") + + +def _format_pairs(lst, specials=(), sep="; "): + """ + specials: A lower-cased list of keys that will not be quoted. + """ + vals = [] + for k, v in lst: + if v is None: + vals.append(k) + else: + if k.lower() not in specials and _has_special(v): + v = ESCAPE.sub(r"\\\1", v) + v = '"%s"' % v + vals.append("%s=%s" % (k, v)) + return sep.join(vals) + + +def _format_set_cookie_pairs(lst): + return _format_pairs( + lst, + specials=("expires", "path") + ) + + +def _parse_set_cookie_pairs(s): + """ + For Set-Cookie, we support multiple cookies as described in RFC2109. + This function therefore returns a list of lists. + """ + pairs, off_ = _read_pairs(s) + return pairs + + +def parse_set_cookie_header(line): + """ + Parse a Set-Cookie header value + + Returns a (name, value, attrs) tuple, or None, where attrs is an + ODictCaseless set of attributes. No attempt is made to parse attribute + values - they are treated purely as strings. + """ + pairs = _parse_set_cookie_pairs(line) + if pairs: + return pairs[0][0], pairs[0][1], odict.ODictCaseless(pairs[1:]) + + +def format_set_cookie_header(name, value, attrs): + """ + Formats a Set-Cookie header value. + """ + pairs = [[name, value]] + pairs.extend(attrs.lst) + return _format_set_cookie_pairs(pairs) + + +def parse_cookie_header(line): + """ + Parse a Cookie header value. + Returns a (possibly empty) ODict object. + """ + pairs, off_ = _read_pairs(line) + return odict.ODict(pairs) + + +def format_cookie_header(od): + """ + Formats a Cookie header value. + """ + return _format_pairs(od.lst) diff --git a/netlib/http/headers.py b/netlib/http/headers.py new file mode 100644 index 00000000..78404796 --- /dev/null +++ b/netlib/http/headers.py @@ -0,0 +1,204 @@ +""" + +Unicode Handling +---------------- +See also: http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/ +""" +from __future__ import absolute_import, print_function, division +import copy +try: + from collections.abc import MutableMapping +except ImportError: # pragma: nocover + from collections import MutableMapping # Workaround for Python < 3.3 + + +import six + +from netlib.utils import always_byte_args, always_bytes, Serializable + +if six.PY2: # pragma: nocover + _native = lambda x: x + _always_bytes = lambda x: x + _always_byte_args = lambda x: x +else: + # While headers _should_ be ASCII, it's not uncommon for certain headers to be utf-8 encoded. + _native = lambda x: x.decode("utf-8", "surrogateescape") + _always_bytes = lambda x: always_bytes(x, "utf-8", "surrogateescape") + _always_byte_args = always_byte_args("utf-8", "surrogateescape") + + +class Headers(MutableMapping, Serializable): + """ + Header class which allows both convenient access to individual headers as well as + direct access to the underlying raw data. Provides a full dictionary interface. + + Example: + + .. code-block:: python + + # Create headers with keyword arguments + >>> h = Headers(host="example.com", content_type="application/xml") + + # Headers mostly behave like a normal dict. + >>> h["Host"] + "example.com" + + # HTTP Headers are case insensitive + >>> h["host"] + "example.com" + + # Headers can also be creatd from a list of raw (header_name, header_value) byte tuples + >>> h = Headers([ + [b"Host",b"example.com"], + [b"Accept",b"text/html"], + [b"accept",b"application/xml"] + ]) + + # Multiple headers are folded into a single header as per RFC7230 + >>> h["Accept"] + "text/html, application/xml" + + # Setting a header removes all existing headers with the same name. + >>> h["Accept"] = "application/text" + >>> h["Accept"] + "application/text" + + # bytes(h) returns a HTTP1 header block. + >>> print(bytes(h)) + Host: example.com + Accept: application/text + + # For full control, the raw header fields can be accessed + >>> h.fields + + Caveats: + For use with the "Set-Cookie" header, see :py:meth:`get_all`. + """ + + @_always_byte_args + def __init__(self, fields=None, **headers): + """ + Args: + fields: (optional) list of ``(name, value)`` header byte tuples, + e.g. ``[(b"Host", b"example.com")]``. All names and values must be bytes. + **headers: Additional headers to set. Will overwrite existing values from `fields`. + For convenience, underscores in header names will be transformed to dashes - + this behaviour does not extend to other methods. + If ``**headers`` contains multiple keys that have equal ``.lower()`` s, + the behavior is undefined. + """ + self.fields = fields or [] + + for name, value in self.fields: + if not isinstance(name, bytes) or not isinstance(value, bytes): + raise ValueError("Headers passed as fields must be bytes.") + + # content_type -> content-type + headers = { + _always_bytes(name).replace(b"_", b"-"): value + for name, value in six.iteritems(headers) + } + self.update(headers) + + def __bytes__(self): + if self.fields: + return b"\r\n".join(b": ".join(field) for field in self.fields) + b"\r\n" + else: + return b"" + + if six.PY2: # pragma: nocover + __str__ = __bytes__ + + @_always_byte_args + def __getitem__(self, name): + values = self.get_all(name) + if not values: + raise KeyError(name) + return ", ".join(values) + + @_always_byte_args + def __setitem__(self, name, value): + idx = self._index(name) + + # To please the human eye, we insert at the same position the first existing header occured. + if idx is not None: + del self[name] + self.fields.insert(idx, [name, value]) + else: + self.fields.append([name, value]) + + @_always_byte_args + def __delitem__(self, name): + if name not in self: + raise KeyError(name) + name = name.lower() + self.fields = [ + field for field in self.fields + if name != field[0].lower() + ] + + def __iter__(self): + seen = set() + for name, _ in self.fields: + name_lower = name.lower() + if name_lower not in seen: + seen.add(name_lower) + yield _native(name) + + def __len__(self): + return len(set(name.lower() for name, _ in self.fields)) + + # __hash__ = object.__hash__ + + def _index(self, name): + name = name.lower() + for i, field in enumerate(self.fields): + if field[0].lower() == name: + return i + return None + + def __eq__(self, other): + if isinstance(other, Headers): + return self.fields == other.fields + return False + + def __ne__(self, other): + return not self.__eq__(other) + + @_always_byte_args + def get_all(self, name): + """ + Like :py:meth:`get`, but does not fold multiple headers into a single one. + This is useful for Set-Cookie headers, which do not support folding. + + See also: https://tools.ietf.org/html/rfc7230#section-3.2.2 + """ + name_lower = name.lower() + values = [_native(value) for n, value in self.fields if n.lower() == name_lower] + return values + + @_always_byte_args + def set_all(self, name, values): + """ + Explicitly set multiple headers for the given key. + See: :py:meth:`get_all` + """ + values = map(_always_bytes, values) # _always_byte_args does not fix lists + if name in self: + del self[name] + self.fields.extend( + [name, value] for value in values + ) + + def copy(self): + return Headers(copy.copy(self.fields)) + + def get_state(self): + return tuple(tuple(field) for field in self.fields) + + def set_state(self, state): + self.fields = [list(field) for field in state] + + @classmethod + def from_state(cls, state): + return cls([list(field) for field in state]) \ No newline at end of file diff --git a/netlib/http/http1/__init__.py b/netlib/http/http1/__init__.py new file mode 100644 index 00000000..2aa7e26a --- /dev/null +++ b/netlib/http/http1/__init__.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, print_function, division +from .read import ( + read_request, read_request_head, + read_response, read_response_head, + read_body, + connection_close, + expected_http_body_size, +) +from .assemble import ( + assemble_request, assemble_request_head, + assemble_response, assemble_response_head, + assemble_body, +) + + +__all__ = [ + "read_request", "read_request_head", + "read_response", "read_response_head", + "read_body", + "connection_close", + "expected_http_body_size", + "assemble_request", "assemble_request_head", + "assemble_response", "assemble_response_head", + "assemble_body", +] diff --git a/netlib/http/http1/assemble.py b/netlib/http/http1/assemble.py new file mode 100644 index 00000000..785ee8d3 --- /dev/null +++ b/netlib/http/http1/assemble.py @@ -0,0 +1,104 @@ +from __future__ import absolute_import, print_function, division + +from ... import utils +import itertools +from ...exceptions import HttpException +from .. import CONTENT_MISSING + + +def assemble_request(request): + if request.content == CONTENT_MISSING: + raise HttpException("Cannot assemble flow with CONTENT_MISSING") + head = assemble_request_head(request) + body = b"".join(assemble_body(request.data.headers, [request.data.content])) + return head + body + + +def assemble_request_head(request): + first_line = _assemble_request_line(request.data) + headers = _assemble_request_headers(request.data) + return b"%s\r\n%s\r\n" % (first_line, headers) + + +def assemble_response(response): + if response.content == CONTENT_MISSING: + raise HttpException("Cannot assemble flow with CONTENT_MISSING") + head = assemble_response_head(response) + body = b"".join(assemble_body(response.data.headers, [response.data.content])) + return head + body + + +def assemble_response_head(response): + first_line = _assemble_response_line(response.data) + headers = _assemble_response_headers(response.data) + return b"%s\r\n%s\r\n" % (first_line, headers) + + +def assemble_body(headers, body_chunks): + if "chunked" in headers.get("transfer-encoding", "").lower(): + for chunk in body_chunks: + if chunk: + yield b"%x\r\n%s\r\n" % (len(chunk), chunk) + yield b"0\r\n\r\n" + else: + for chunk in body_chunks: + yield chunk + + +def _assemble_request_line(request_data): + """ + Args: + request_data (netlib.http.request.RequestData) + """ + form = request_data.first_line_format + if form == "relative": + return b"%s %s %s" % ( + request_data.method, + request_data.path, + request_data.http_version + ) + elif form == "authority": + return b"%s %s:%d %s" % ( + request_data.method, + request_data.host, + request_data.port, + request_data.http_version + ) + elif form == "absolute": + return b"%s %s://%s:%d%s %s" % ( + request_data.method, + request_data.scheme, + request_data.host, + request_data.port, + request_data.path, + request_data.http_version + ) + else: + raise RuntimeError("Invalid request form") + + +def _assemble_request_headers(request_data): + """ + Args: + request_data (netlib.http.request.RequestData) + """ + headers = request_data.headers.copy() + if "host" not in headers and request_data.scheme and request_data.host and request_data.port: + headers["host"] = utils.hostport( + request_data.scheme, + request_data.host, + request_data.port + ) + return bytes(headers) + + +def _assemble_response_line(response_data): + return b"%s %d %s" % ( + response_data.http_version, + response_data.status_code, + response_data.reason, + ) + + +def _assemble_response_headers(response): + return bytes(response.headers) diff --git a/netlib/http/http1/read.py b/netlib/http/http1/read.py new file mode 100644 index 00000000..6e3a1b93 --- /dev/null +++ b/netlib/http/http1/read.py @@ -0,0 +1,362 @@ +from __future__ import absolute_import, print_function, division +import time +import sys +import re + +from ... import utils +from ...exceptions import HttpReadDisconnect, HttpSyntaxException, HttpException, TcpDisconnect +from .. import Request, Response, Headers + + +def read_request(rfile, body_size_limit=None): + request = read_request_head(rfile) + expected_body_size = expected_http_body_size(request) + request.data.content = b"".join(read_body(rfile, expected_body_size, limit=body_size_limit)) + request.timestamp_end = time.time() + return request + + +def read_request_head(rfile): + """ + Parse an HTTP request head (request line + headers) from an input stream + + Args: + rfile: The input stream + + Returns: + The HTTP request object (without body) + + Raises: + HttpReadDisconnect: No bytes can be read from rfile. + HttpSyntaxException: The input is malformed HTTP. + HttpException: Any other error occured. + """ + timestamp_start = time.time() + if hasattr(rfile, "reset_timestamps"): + rfile.reset_timestamps() + + form, method, scheme, host, port, path, http_version = _read_request_line(rfile) + headers = _read_headers(rfile) + + if hasattr(rfile, "first_byte_timestamp"): + # more accurate timestamp_start + timestamp_start = rfile.first_byte_timestamp + + return Request( + form, method, scheme, host, port, path, http_version, headers, None, timestamp_start + ) + + +def read_response(rfile, request, body_size_limit=None): + response = read_response_head(rfile) + expected_body_size = expected_http_body_size(request, response) + response.data.content = b"".join(read_body(rfile, expected_body_size, body_size_limit)) + response.timestamp_end = time.time() + return response + + +def read_response_head(rfile): + """ + Parse an HTTP response head (response line + headers) from an input stream + + Args: + rfile: The input stream + + Returns: + The HTTP request object (without body) + + Raises: + HttpReadDisconnect: No bytes can be read from rfile. + HttpSyntaxException: The input is malformed HTTP. + HttpException: Any other error occured. + """ + + timestamp_start = time.time() + if hasattr(rfile, "reset_timestamps"): + rfile.reset_timestamps() + + http_version, status_code, message = _read_response_line(rfile) + headers = _read_headers(rfile) + + if hasattr(rfile, "first_byte_timestamp"): + # more accurate timestamp_start + timestamp_start = rfile.first_byte_timestamp + + return Response(http_version, status_code, message, headers, None, timestamp_start) + + +def read_body(rfile, expected_size, limit=None, max_chunk_size=4096): + """ + Read an HTTP message body + + Args: + rfile: The input stream + expected_size: The expected body size (see :py:meth:`expected_body_size`) + limit: Maximum body size + max_chunk_size: Maximium chunk size that gets yielded + + Returns: + A generator that yields byte chunks of the content. + + Raises: + HttpException, if an error occurs + + Caveats: + max_chunk_size is not considered if the transfer encoding is chunked. + """ + if not limit or limit < 0: + limit = sys.maxsize + if not max_chunk_size: + max_chunk_size = limit + + if expected_size is None: + for x in _read_chunked(rfile, limit): + yield x + elif expected_size >= 0: + if limit is not None and expected_size > limit: + raise HttpException( + "HTTP Body too large. " + "Limit is {}, content length was advertised as {}".format(limit, expected_size) + ) + bytes_left = expected_size + while bytes_left: + chunk_size = min(bytes_left, max_chunk_size) + content = rfile.read(chunk_size) + if len(content) < chunk_size: + raise HttpException("Unexpected EOF") + yield content + bytes_left -= chunk_size + else: + bytes_left = limit + while bytes_left: + chunk_size = min(bytes_left, max_chunk_size) + content = rfile.read(chunk_size) + if not content: + return + yield content + bytes_left -= chunk_size + not_done = rfile.read(1) + if not_done: + raise HttpException("HTTP body too large. Limit is {}.".format(limit)) + + +def connection_close(http_version, headers): + """ + Checks the message to see if the client connection should be closed + according to RFC 2616 Section 8.1. + """ + # At first, check if we have an explicit Connection header. + if "connection" in headers: + tokens = utils.get_header_tokens(headers, "connection") + if "close" in tokens: + return True + elif "keep-alive" in tokens: + return False + + # If we don't have a Connection header, HTTP 1.1 connections are assumed to + # be persistent + return http_version != "HTTP/1.1" and http_version != b"HTTP/1.1" # FIXME: Remove one case. + + +def expected_http_body_size(request, response=None): + """ + Returns: + The expected body length: + - a positive integer, if the size is known in advance + - None, if the size in unknown in advance (chunked encoding) + - -1, if all data should be read until end of stream. + + Raises: + HttpSyntaxException, if the content length header is invalid + """ + # Determine response size according to + # http://tools.ietf.org/html/rfc7230#section-3.3 + if not response: + headers = request.headers + response_code = None + is_request = True + else: + headers = response.headers + response_code = response.status_code + is_request = False + + if is_request: + if headers.get("expect", "").lower() == "100-continue": + return 0 + else: + if request.method.upper() == "HEAD": + return 0 + if 100 <= response_code <= 199: + return 0 + if response_code == 200 and request.method.upper() == "CONNECT": + return 0 + if response_code in (204, 304): + return 0 + + if "chunked" in headers.get("transfer-encoding", "").lower(): + return None + if "content-length" in headers: + try: + size = int(headers["content-length"]) + if size < 0: + raise ValueError() + return size + except ValueError: + raise HttpSyntaxException("Unparseable Content Length") + if is_request: + return 0 + return -1 + + +def _get_first_line(rfile): + try: + line = rfile.readline() + if line == b"\r\n" or line == b"\n": + # Possible leftover from previous message + line = rfile.readline() + except TcpDisconnect: + raise HttpReadDisconnect("Remote disconnected") + if not line: + raise HttpReadDisconnect("Remote disconnected") + return line.strip() + + +def _read_request_line(rfile): + try: + line = _get_first_line(rfile) + except HttpReadDisconnect: + # We want to provide a better error message. + raise HttpReadDisconnect("Client disconnected") + + try: + method, path, http_version = line.split(b" ") + + if path == b"*" or path.startswith(b"/"): + form = "relative" + scheme, host, port = None, None, None + elif method == b"CONNECT": + form = "authority" + host, port = _parse_authority_form(path) + scheme, path = None, None + else: + form = "absolute" + scheme, host, port, path = utils.parse_url(path) + + _check_http_version(http_version) + except ValueError: + raise HttpSyntaxException("Bad HTTP request line: {}".format(line)) + + return form, method, scheme, host, port, path, http_version + + +def _parse_authority_form(hostport): + """ + Returns (host, port) if hostport is a valid authority-form host specification. + http://tools.ietf.org/html/draft-luotonen-web-proxy-tunneling-01 section 3.1 + + Raises: + ValueError, if the input is malformed + """ + try: + host, port = hostport.split(b":") + port = int(port) + if not utils.is_valid_host(host) or not utils.is_valid_port(port): + raise ValueError() + except ValueError: + raise HttpSyntaxException("Invalid host specification: {}".format(hostport)) + + return host, port + + +def _read_response_line(rfile): + try: + line = _get_first_line(rfile) + except HttpReadDisconnect: + # We want to provide a better error message. + raise HttpReadDisconnect("Server disconnected") + + try: + + parts = line.split(b" ", 2) + if len(parts) == 2: # handle missing message gracefully + parts.append(b"") + + http_version, status_code, message = parts + status_code = int(status_code) + _check_http_version(http_version) + + except ValueError: + raise HttpSyntaxException("Bad HTTP response line: {}".format(line)) + + return http_version, status_code, message + + +def _check_http_version(http_version): + if not re.match(br"^HTTP/\d\.\d$", http_version): + raise HttpSyntaxException("Unknown HTTP version: {}".format(http_version)) + + +def _read_headers(rfile): + """ + Read a set of headers. + Stop once a blank line is reached. + + Returns: + A headers object + + Raises: + HttpSyntaxException + """ + ret = [] + while True: + line = rfile.readline() + if not line or line == b"\r\n" or line == b"\n": + break + if line[0] in b" \t": + if not ret: + raise HttpSyntaxException("Invalid headers") + # continued header + ret[-1][1] = ret[-1][1] + b'\r\n ' + line.strip() + else: + try: + name, value = line.split(b":", 1) + value = value.strip() + if not name: + raise ValueError() + ret.append([name, value]) + except ValueError: + raise HttpSyntaxException("Invalid headers") + return Headers(ret) + + +def _read_chunked(rfile, limit=sys.maxsize): + """ + Read a HTTP body with chunked transfer encoding. + + Args: + rfile: the input file + limit: A positive integer + """ + total = 0 + while True: + line = rfile.readline(128) + if line == b"": + raise HttpException("Connection closed prematurely") + if line != b"\r\n" and line != b"\n": + try: + length = int(line, 16) + except ValueError: + raise HttpSyntaxException("Invalid chunked encoding length: {}".format(line)) + total += length + if total > limit: + raise HttpException( + "HTTP Body too large. Limit is {}, " + "chunked content longer than {}".format(limit, total) + ) + chunk = rfile.read(length) + suffix = rfile.readline(5) + if suffix != b"\r\n": + raise HttpSyntaxException("Malformed chunked body") + if length == 0: + return + yield chunk diff --git a/netlib/http/http2/__init__.py b/netlib/http/http2/__init__.py new file mode 100644 index 00000000..7043d36f --- /dev/null +++ b/netlib/http/http2/__init__.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import, print_function, division +from .connections import HTTP2Protocol + +__all__ = [ + "HTTP2Protocol" +] diff --git a/netlib/http/http2/connections.py b/netlib/http/http2/connections.py new file mode 100644 index 00000000..52fa7193 --- /dev/null +++ b/netlib/http/http2/connections.py @@ -0,0 +1,426 @@ +from __future__ import (absolute_import, print_function, division) +import itertools +import time + +from hpack.hpack import Encoder, Decoder +from ... import utils +from .. import Headers, Response, Request + +from hyperframe import frame + + +class TCPHandler(object): + + def __init__(self, rfile, wfile=None): + self.rfile = rfile + self.wfile = wfile + + +class HTTP2Protocol(object): + + ERROR_CODES = utils.BiDi( + NO_ERROR=0x0, + PROTOCOL_ERROR=0x1, + INTERNAL_ERROR=0x2, + FLOW_CONTROL_ERROR=0x3, + SETTINGS_TIMEOUT=0x4, + STREAM_CLOSED=0x5, + FRAME_SIZE_ERROR=0x6, + REFUSED_STREAM=0x7, + CANCEL=0x8, + COMPRESSION_ERROR=0x9, + CONNECT_ERROR=0xa, + ENHANCE_YOUR_CALM=0xb, + INADEQUATE_SECURITY=0xc, + HTTP_1_1_REQUIRED=0xd + ) + + CLIENT_CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + + HTTP2_DEFAULT_SETTINGS = { + frame.SettingsFrame.HEADER_TABLE_SIZE: 4096, + frame.SettingsFrame.ENABLE_PUSH: 1, + frame.SettingsFrame.MAX_CONCURRENT_STREAMS: None, + frame.SettingsFrame.INITIAL_WINDOW_SIZE: 2 ** 16 - 1, + frame.SettingsFrame.MAX_FRAME_SIZE: 2 ** 14, + frame.SettingsFrame.MAX_HEADER_LIST_SIZE: None, + } + + def __init__( + self, + tcp_handler=None, + rfile=None, + wfile=None, + is_server=False, + dump_frames=False, + encoder=None, + decoder=None, + unhandled_frame_cb=None, + ): + self.tcp_handler = tcp_handler or TCPHandler(rfile, wfile) + self.is_server = is_server + self.dump_frames = dump_frames + self.encoder = encoder or Encoder() + self.decoder = decoder or Decoder() + self.unhandled_frame_cb = unhandled_frame_cb + + self.http2_settings = self.HTTP2_DEFAULT_SETTINGS.copy() + self.current_stream_id = None + self.connection_preface_performed = False + + def read_request( + self, + __rfile, + include_body=True, + body_size_limit=None, + allow_empty=False, + ): + if body_size_limit is not None: + raise NotImplementedError() + + self.perform_connection_preface() + + timestamp_start = time.time() + if hasattr(self.tcp_handler.rfile, "reset_timestamps"): + self.tcp_handler.rfile.reset_timestamps() + + stream_id, headers, body = self._receive_transmission( + include_body=include_body, + ) + + if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): + # more accurate timestamp_start + timestamp_start = self.tcp_handler.rfile.first_byte_timestamp + + timestamp_end = time.time() + + authority = headers.get(':authority', b'') + method = headers.get(':method', 'GET') + scheme = headers.get(':scheme', 'https') + path = headers.get(':path', '/') + host = None + port = None + + if path == '*' or path.startswith("/"): + form_in = "relative" + elif method == 'CONNECT': + form_in = "authority" + if ":" in authority: + host, port = authority.split(":", 1) + else: + host = authority + else: + form_in = "absolute" + # FIXME: verify if path or :host contains what we need + scheme, host, port, _ = utils.parse_url(path) + scheme = scheme.decode('ascii') + host = host.decode('ascii') + + if host is None: + host = 'localhost' + if port is None: + port = 80 if scheme == 'http' else 443 + port = int(port) + + request = Request( + form_in, + method.encode('ascii'), + scheme.encode('ascii'), + host.encode('ascii'), + port, + path.encode('ascii'), + b"HTTP/2.0", + headers, + body, + timestamp_start, + timestamp_end, + ) + request.stream_id = stream_id + + return request + + def read_response( + self, + __rfile, + request_method=b'', + body_size_limit=None, + include_body=True, + stream_id=None, + ): + if body_size_limit is not None: + raise NotImplementedError() + + self.perform_connection_preface() + + timestamp_start = time.time() + if hasattr(self.tcp_handler.rfile, "reset_timestamps"): + self.tcp_handler.rfile.reset_timestamps() + + stream_id, headers, body = self._receive_transmission( + stream_id=stream_id, + include_body=include_body, + ) + + if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): + # more accurate timestamp_start + timestamp_start = self.tcp_handler.rfile.first_byte_timestamp + + if include_body: + timestamp_end = time.time() + else: + timestamp_end = None + + response = Response( + b"HTTP/2.0", + int(headers.get(':status', 502)), + b'', + headers, + body, + timestamp_start=timestamp_start, + timestamp_end=timestamp_end, + ) + response.stream_id = stream_id + + return response + + def assemble(self, message): + if isinstance(message, Request): + return self.assemble_request(message) + elif isinstance(message, Response): + return self.assemble_response(message) + else: + raise ValueError("HTTP message not supported.") + + def assemble_request(self, request): + assert isinstance(request, Request) + + authority = self.tcp_handler.sni if self.tcp_handler.sni else self.tcp_handler.address.host + if self.tcp_handler.address.port != 443: + authority += ":%d" % self.tcp_handler.address.port + + headers = request.headers.copy() + + if ':authority' not in headers: + headers.fields.insert(0, (b':authority', authority.encode('ascii'))) + if ':scheme' not in headers: + headers.fields.insert(0, (b':scheme', request.scheme.encode('ascii'))) + if ':path' not in headers: + headers.fields.insert(0, (b':path', request.path.encode('ascii'))) + if ':method' not in headers: + headers.fields.insert(0, (b':method', request.method.encode('ascii'))) + + if hasattr(request, 'stream_id'): + stream_id = request.stream_id + else: + stream_id = self._next_stream_id() + + return list(itertools.chain( + self._create_headers(headers, stream_id, end_stream=(request.body is None or len(request.body) == 0)), + self._create_body(request.body, stream_id))) + + def assemble_response(self, response): + assert isinstance(response, Response) + + headers = response.headers.copy() + + if ':status' not in headers: + headers.fields.insert(0, (b':status', str(response.status_code).encode('ascii'))) + + if hasattr(response, 'stream_id'): + stream_id = response.stream_id + else: + stream_id = self._next_stream_id() + + return list(itertools.chain( + self._create_headers(headers, stream_id, end_stream=(response.body is None or len(response.body) == 0)), + self._create_body(response.body, stream_id), + )) + + def perform_connection_preface(self, force=False): + if force or not self.connection_preface_performed: + if self.is_server: + self.perform_server_connection_preface(force) + else: + self.perform_client_connection_preface(force) + + def perform_server_connection_preface(self, force=False): + if force or not self.connection_preface_performed: + self.connection_preface_performed = True + + magic_length = len(self.CLIENT_CONNECTION_PREFACE) + magic = self.tcp_handler.rfile.safe_read(magic_length) + assert magic == self.CLIENT_CONNECTION_PREFACE + + frm = frame.SettingsFrame(settings={ + frame.SettingsFrame.ENABLE_PUSH: 0, + frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 1, + }) + self.send_frame(frm, hide=True) + self._receive_settings(hide=True) + + def perform_client_connection_preface(self, force=False): + if force or not self.connection_preface_performed: + self.connection_preface_performed = True + + self.tcp_handler.wfile.write(self.CLIENT_CONNECTION_PREFACE) + + self.send_frame(frame.SettingsFrame(), hide=True) + self._receive_settings(hide=True) # server announces own settings + self._receive_settings(hide=True) # server acks my settings + + def send_frame(self, frm, hide=False): + raw_bytes = frm.serialize() + self.tcp_handler.wfile.write(raw_bytes) + self.tcp_handler.wfile.flush() + if not hide and self.dump_frames: # pragma no cover + print(frm.human_readable(">>")) + + def read_frame(self, hide=False): + while True: + frm = utils.http2_read_frame(self.tcp_handler.rfile) + if not hide and self.dump_frames: # pragma no cover + print(frm.human_readable("<<")) + + if isinstance(frm, frame.PingFrame): + raw_bytes = frame.PingFrame(flags=['ACK'], payload=frm.payload).serialize() + self.tcp_handler.wfile.write(raw_bytes) + self.tcp_handler.wfile.flush() + continue + if isinstance(frm, frame.SettingsFrame) and 'ACK' not in frm.flags: + self._apply_settings(frm.settings, hide) + if isinstance(frm, frame.DataFrame) and frm.flow_controlled_length > 0: + self._update_flow_control_window(frm.stream_id, frm.flow_controlled_length) + return frm + + def check_alpn(self): + alp = self.tcp_handler.get_alpn_proto_negotiated() + if alp != b'h2': + raise NotImplementedError( + "HTTP2Protocol can not handle unknown ALP: %s" % alp) + return True + + def _handle_unexpected_frame(self, frm): + if isinstance(frm, frame.SettingsFrame): + return + if self.unhandled_frame_cb: + self.unhandled_frame_cb(frm) + + def _receive_settings(self, hide=False): + while True: + frm = self.read_frame(hide) + if isinstance(frm, frame.SettingsFrame): + break + else: + self._handle_unexpected_frame(frm) + + def _next_stream_id(self): + if self.current_stream_id is None: + if self.is_server: + # servers must use even stream ids + self.current_stream_id = 2 + else: + # clients must use odd stream ids + self.current_stream_id = 1 + else: + self.current_stream_id += 2 + return self.current_stream_id + + def _apply_settings(self, settings, hide=False): + for setting, value in settings.items(): + old_value = self.http2_settings[setting] + if not old_value: + old_value = '-' + self.http2_settings[setting] = value + + frm = frame.SettingsFrame(flags=['ACK']) + self.send_frame(frm, hide) + + def _update_flow_control_window(self, stream_id, increment): + frm = frame.WindowUpdateFrame(stream_id=0, window_increment=increment) + self.send_frame(frm) + frm = frame.WindowUpdateFrame(stream_id=stream_id, window_increment=increment) + self.send_frame(frm) + + def _create_headers(self, headers, stream_id, end_stream=True): + def frame_cls(chunks): + for i in chunks: + if i == 0: + yield frame.HeadersFrame, i + else: + yield frame.ContinuationFrame, i + + header_block_fragment = self.encoder.encode(headers.fields) + + chunk_size = self.http2_settings[frame.SettingsFrame.MAX_FRAME_SIZE] + chunks = range(0, len(header_block_fragment), chunk_size) + frms = [frm_cls( + flags=[], + stream_id=stream_id, + data=header_block_fragment[i:i+chunk_size]) for frm_cls, i in frame_cls(chunks)] + + frms[-1].flags.add('END_HEADERS') + if end_stream: + frms[0].flags.add('END_STREAM') + + if self.dump_frames: # pragma no cover + for frm in frms: + print(frm.human_readable(">>")) + + return [frm.serialize() for frm in frms] + + def _create_body(self, body, stream_id): + if body is None or len(body) == 0: + return b'' + + chunk_size = self.http2_settings[frame.SettingsFrame.MAX_FRAME_SIZE] + chunks = range(0, len(body), chunk_size) + frms = [frame.DataFrame( + flags=[], + stream_id=stream_id, + data=body[i:i+chunk_size]) for i in chunks] + frms[-1].flags.add('END_STREAM') + + if self.dump_frames: # pragma no cover + for frm in frms: + print(frm.human_readable(">>")) + + return [frm.serialize() for frm in frms] + + def _receive_transmission(self, stream_id=None, include_body=True): + if not include_body: + raise NotImplementedError() + + body_expected = True + + header_blocks = b'' + body = b'' + + while True: + frm = self.read_frame() + if ( + (isinstance(frm, frame.HeadersFrame) or isinstance(frm, frame.ContinuationFrame)) and + (stream_id is None or frm.stream_id == stream_id) + ): + stream_id = frm.stream_id + header_blocks += frm.data + if 'END_STREAM' in frm.flags: + body_expected = False + if 'END_HEADERS' in frm.flags: + break + else: + self._handle_unexpected_frame(frm) + + while body_expected: + frm = self.read_frame() + if isinstance(frm, frame.DataFrame) and frm.stream_id == stream_id: + body += frm.data + if 'END_STREAM' in frm.flags: + break + else: + self._handle_unexpected_frame(frm) + + headers = Headers( + [[k.encode('ascii'), v.encode('ascii')] for k, v in self.decoder.decode(header_blocks)] + ) + + return stream_id, headers, body diff --git a/netlib/http/message.py b/netlib/http/message.py new file mode 100644 index 00000000..e3d8ce37 --- /dev/null +++ b/netlib/http/message.py @@ -0,0 +1,222 @@ +from __future__ import absolute_import, print_function, division + +import warnings + +import six + +from .headers import Headers +from .. import encoding, utils + +CONTENT_MISSING = 0 + +if six.PY2: # pragma: nocover + _native = lambda x: x + _always_bytes = lambda x: x +else: + # While the HTTP head _should_ be ASCII, it's not uncommon for certain headers to be utf-8 encoded. + _native = lambda x: x.decode("utf-8", "surrogateescape") + _always_bytes = lambda x: utils.always_bytes(x, "utf-8", "surrogateescape") + + +class MessageData(utils.Serializable): + def __eq__(self, other): + if isinstance(other, MessageData): + return self.__dict__ == other.__dict__ + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def set_state(self, state): + for k, v in state.items(): + if k == "headers": + v = Headers.from_state(v) + setattr(self, k, v) + + def get_state(self): + state = vars(self).copy() + state["headers"] = state["headers"].get_state() + return state + + @classmethod + def from_state(cls, state): + state["headers"] = Headers.from_state(state["headers"]) + return cls(**state) + + +class Message(utils.Serializable): + def __init__(self, data): + self.data = data + + def __eq__(self, other): + if isinstance(other, Message): + return self.data == other.data + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def get_state(self): + return self.data.get_state() + + def set_state(self, state): + self.data.set_state(state) + + @classmethod + def from_state(cls, state): + return cls(**state) + + @property + def headers(self): + """ + Message headers object + + Returns: + netlib.http.Headers + """ + return self.data.headers + + @headers.setter + def headers(self, h): + self.data.headers = h + + @property + def content(self): + """ + The raw (encoded) HTTP message body + + See also: :py:attr:`text` + """ + return self.data.content + + @content.setter + def content(self, content): + self.data.content = content + if isinstance(content, bytes): + self.headers["content-length"] = str(len(content)) + + @property + def http_version(self): + """ + Version string, e.g. "HTTP/1.1" + """ + return _native(self.data.http_version) + + @http_version.setter + def http_version(self, http_version): + self.data.http_version = _always_bytes(http_version) + + @property + def timestamp_start(self): + """ + First byte timestamp + """ + return self.data.timestamp_start + + @timestamp_start.setter + def timestamp_start(self, timestamp_start): + self.data.timestamp_start = timestamp_start + + @property + def timestamp_end(self): + """ + Last byte timestamp + """ + return self.data.timestamp_end + + @timestamp_end.setter + def timestamp_end(self, timestamp_end): + self.data.timestamp_end = timestamp_end + + @property + def text(self): + """ + The decoded HTTP message body. + Decoded contents are not cached, so accessing this attribute repeatedly is relatively expensive. + + .. note:: + This is not implemented yet. + + See also: :py:attr:`content`, :py:class:`decoded` + """ + # This attribute should be called text, because that's what requests does. + raise NotImplementedError() + + @text.setter + def text(self, text): + raise NotImplementedError() + + def decode(self): + """ + Decodes body based on the current Content-Encoding header, then + removes the header. If there is no Content-Encoding header, no + action is taken. + + Returns: + True, if decoding succeeded. + False, otherwise. + """ + ce = self.headers.get("content-encoding") + data = encoding.decode(ce, self.content) + if data is None: + return False + self.content = data + self.headers.pop("content-encoding", None) + return True + + def encode(self, e): + """ + Encodes body with the encoding e, where e is "gzip", "deflate" or "identity". + + Returns: + True, if decoding succeeded. + False, otherwise. + """ + data = encoding.encode(e, self.content) + if data is None: + return False + self.content = data + self.headers["content-encoding"] = e + return True + + # Legacy + + @property + def body(self): # pragma: nocover + warnings.warn(".body is deprecated, use .content instead.", DeprecationWarning) + return self.content + + @body.setter + def body(self, body): # pragma: nocover + warnings.warn(".body is deprecated, use .content instead.", DeprecationWarning) + self.content = body + + +class decoded(object): + """ + A context manager that decodes a request or response, and then + re-encodes it with the same encoding after execution of the block. + + Example: + + .. code-block:: python + + with decoded(request): + request.content = request.content.replace("foo", "bar") + """ + + def __init__(self, message): + self.message = message + ce = message.headers.get("content-encoding") + if ce in encoding.ENCODINGS: + self.ce = ce + else: + self.ce = None + + def __enter__(self): + if self.ce: + self.message.decode() + + def __exit__(self, type, value, tb): + if self.ce: + self.message.encode(self.ce) diff --git a/netlib/http/request.py b/netlib/http/request.py new file mode 100644 index 00000000..b9076c0f --- /dev/null +++ b/netlib/http/request.py @@ -0,0 +1,356 @@ +from __future__ import absolute_import, print_function, division + +import warnings + +import six +from six.moves import urllib + +from netlib import utils +from netlib.http import cookies +from netlib.odict import ODict +from .. import encoding +from .headers import Headers +from .message import Message, _native, _always_bytes, MessageData + + +class RequestData(MessageData): + def __init__(self, first_line_format, method, scheme, host, port, path, http_version, headers=None, content=None, + timestamp_start=None, timestamp_end=None): + if not isinstance(headers, Headers): + headers = Headers(headers) + + self.first_line_format = first_line_format + self.method = method + self.scheme = scheme + self.host = host + self.port = port + self.path = path + self.http_version = http_version + self.headers = headers + self.content = content + self.timestamp_start = timestamp_start + self.timestamp_end = timestamp_end + + +class Request(Message): + """ + An HTTP request. + """ + def __init__(self, *args, **kwargs): + data = RequestData(*args, **kwargs) + super(Request, self).__init__(data) + + def __repr__(self): + if self.host and self.port: + hostport = "{}:{}".format(self.host, self.port) + else: + hostport = "" + path = self.path or "" + return "Request({} {}{})".format( + self.method, hostport, path + ) + + @property + def first_line_format(self): + """ + HTTP request form as defined in `RFC7230 `_. + + origin-form and asterisk-form are subsumed as "relative". + """ + return self.data.first_line_format + + @first_line_format.setter + def first_line_format(self, first_line_format): + self.data.first_line_format = first_line_format + + @property + def method(self): + """ + HTTP request method, e.g. "GET". + """ + return _native(self.data.method).upper() + + @method.setter + def method(self, method): + self.data.method = _always_bytes(method) + + @property + def scheme(self): + """ + HTTP request scheme, which should be "http" or "https". + """ + return _native(self.data.scheme) + + @scheme.setter + def scheme(self, scheme): + self.data.scheme = _always_bytes(scheme) + + @property + def host(self): + """ + Target host. This may be parsed from the raw request + (e.g. from a ``GET http://example.com/ HTTP/1.1`` request line) + or inferred from the proxy mode (e.g. an IP in transparent mode). + + Setting the host attribute also updates the host header, if present. + """ + + if six.PY2: # pragma: nocover + return self.data.host + + if not self.data.host: + return self.data.host + try: + return self.data.host.decode("idna") + except UnicodeError: + return self.data.host.decode("utf8", "surrogateescape") + + @host.setter + def host(self, host): + if isinstance(host, six.text_type): + try: + # There's no non-strict mode for IDNA encoding. + # We don't want this operation to fail though, so we try + # utf8 as a last resort. + host = host.encode("idna", "strict") + except UnicodeError: + host = host.encode("utf8", "surrogateescape") + + self.data.host = host + + # Update host header + if "host" in self.headers: + if host: + self.headers["host"] = host + else: + self.headers.pop("host") + + @property + def port(self): + """ + Target port + """ + return self.data.port + + @port.setter + def port(self, port): + self.data.port = port + + @property + def path(self): + """ + HTTP request path, e.g. "/index.html". + Guaranteed to start with a slash. + """ + return _native(self.data.path) + + @path.setter + def path(self, path): + self.data.path = _always_bytes(path) + + @property + def url(self): + """ + The URL string, constructed from the request's URL components + """ + return utils.unparse_url(self.scheme, self.host, self.port, self.path) + + @url.setter + def url(self, url): + self.scheme, self.host, self.port, self.path = utils.parse_url(url) + + @property + def pretty_host(self): + """ + Similar to :py:attr:`host`, but using the Host headers as an additional preferred data source. + This is useful in transparent mode where :py:attr:`host` is only an IP address, + but may not reflect the actual destination as the Host header could be spoofed. + """ + return self.headers.get("host", self.host) + + @property + def pretty_url(self): + """ + Like :py:attr:`url`, but using :py:attr:`pretty_host` instead of :py:attr:`host`. + """ + if self.first_line_format == "authority": + return "%s:%d" % (self.pretty_host, self.port) + return utils.unparse_url(self.scheme, self.pretty_host, self.port, self.path) + + @property + def query(self): + """ + The request query string as an :py:class:`ODict` object. + None, if there is no query. + """ + _, _, _, _, query, _ = urllib.parse.urlparse(self.url) + if query: + return ODict(utils.urldecode(query)) + return None + + @query.setter + def query(self, odict): + query = utils.urlencode(odict.lst) + scheme, netloc, path, params, _, fragment = urllib.parse.urlparse(self.url) + _, _, _, self.path = utils.parse_url( + urllib.parse.urlunparse([scheme, netloc, path, params, query, fragment])) + + @property + def cookies(self): + """ + The request cookies. + An empty :py:class:`ODict` object if the cookie monster ate them all. + """ + ret = ODict() + for i in self.headers.get_all("Cookie"): + ret.extend(cookies.parse_cookie_header(i)) + return ret + + @cookies.setter + def cookies(self, odict): + self.headers["cookie"] = cookies.format_cookie_header(odict) + + @property + def path_components(self): + """ + The URL's path components as a list of strings. + Components are unquoted. + """ + _, _, path, _, _, _ = urllib.parse.urlparse(self.url) + return [urllib.parse.unquote(i) for i in path.split("/") if i] + + @path_components.setter + def path_components(self, components): + components = map(lambda x: urllib.parse.quote(x, safe=""), components) + path = "/" + "/".join(components) + scheme, netloc, _, params, query, fragment = urllib.parse.urlparse(self.url) + _, _, _, self.path = utils.parse_url( + urllib.parse.urlunparse([scheme, netloc, path, params, query, fragment])) + + def anticache(self): + """ + Modifies this request to remove headers that might produce a cached + response. That is, we remove ETags and If-Modified-Since headers. + """ + delheaders = [ + "if-modified-since", + "if-none-match", + ] + for i in delheaders: + self.headers.pop(i, None) + + def anticomp(self): + """ + Modifies this request to remove headers that will compress the + resource's data. + """ + self.headers["accept-encoding"] = "identity" + + def constrain_encoding(self): + """ + Limits the permissible Accept-Encoding values, based on what we can + decode appropriately. + """ + accept_encoding = self.headers.get("accept-encoding") + if accept_encoding: + self.headers["accept-encoding"] = ( + ', '.join( + e + for e in encoding.ENCODINGS + if e in accept_encoding + ) + ) + + @property + def urlencoded_form(self): + """ + The URL-encoded form data as an :py:class:`ODict` object. + None if there is no data or the content-type indicates non-form data. + """ + is_valid_content_type = "application/x-www-form-urlencoded" in self.headers.get("content-type", "").lower() + if self.content and is_valid_content_type: + return ODict(utils.urldecode(self.content)) + return None + + @urlencoded_form.setter + def urlencoded_form(self, odict): + """ + Sets the body to the URL-encoded form data, and adds the appropriate content-type header. + This will overwrite the existing content if there is one. + """ + self.headers["content-type"] = "application/x-www-form-urlencoded" + self.content = utils.urlencode(odict.lst) + + @property + def multipart_form(self): + """ + The multipart form data as an :py:class:`ODict` object. + None if there is no data or the content-type indicates non-form data. + """ + is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower() + if self.content and is_valid_content_type: + return ODict(utils.multipartdecode(self.headers,self.content)) + return None + + @multipart_form.setter + def multipart_form(self, value): + raise NotImplementedError() + + # Legacy + + def get_cookies(self): # pragma: nocover + warnings.warn(".get_cookies is deprecated, use .cookies instead.", DeprecationWarning) + return self.cookies + + def set_cookies(self, odict): # pragma: nocover + warnings.warn(".set_cookies is deprecated, use .cookies instead.", DeprecationWarning) + self.cookies = odict + + def get_query(self): # pragma: nocover + warnings.warn(".get_query is deprecated, use .query instead.", DeprecationWarning) + return self.query or ODict([]) + + def set_query(self, odict): # pragma: nocover + warnings.warn(".set_query is deprecated, use .query instead.", DeprecationWarning) + self.query = odict + + def get_path_components(self): # pragma: nocover + warnings.warn(".get_path_components is deprecated, use .path_components instead.", DeprecationWarning) + return self.path_components + + def set_path_components(self, lst): # pragma: nocover + warnings.warn(".set_path_components is deprecated, use .path_components instead.", DeprecationWarning) + self.path_components = lst + + def get_form_urlencoded(self): # pragma: nocover + warnings.warn(".get_form_urlencoded is deprecated, use .urlencoded_form instead.", DeprecationWarning) + return self.urlencoded_form or ODict([]) + + def set_form_urlencoded(self, odict): # pragma: nocover + warnings.warn(".set_form_urlencoded is deprecated, use .urlencoded_form instead.", DeprecationWarning) + self.urlencoded_form = odict + + def get_form_multipart(self): # pragma: nocover + warnings.warn(".get_form_multipart is deprecated, use .multipart_form instead.", DeprecationWarning) + return self.multipart_form or ODict([]) + + @property + def form_in(self): # pragma: nocover + warnings.warn(".form_in is deprecated, use .first_line_format instead.", DeprecationWarning) + return self.first_line_format + + @form_in.setter + def form_in(self, form_in): # pragma: nocover + warnings.warn(".form_in is deprecated, use .first_line_format instead.", DeprecationWarning) + self.first_line_format = form_in + + @property + def form_out(self): # pragma: nocover + warnings.warn(".form_out is deprecated, use .first_line_format instead.", DeprecationWarning) + return self.first_line_format + + @form_out.setter + def form_out(self, form_out): # pragma: nocover + warnings.warn(".form_out is deprecated, use .first_line_format instead.", DeprecationWarning) + self.first_line_format = form_out + diff --git a/netlib/http/response.py b/netlib/http/response.py new file mode 100644 index 00000000..8f4d6215 --- /dev/null +++ b/netlib/http/response.py @@ -0,0 +1,116 @@ +from __future__ import absolute_import, print_function, division + +import warnings + +from . import cookies +from .headers import Headers +from .message import Message, _native, _always_bytes, MessageData +from .. import utils +from ..odict import ODict + + +class ResponseData(MessageData): + def __init__(self, http_version, status_code, reason=None, headers=None, content=None, + timestamp_start=None, timestamp_end=None): + if not isinstance(headers, Headers): + headers = Headers(headers) + + self.http_version = http_version + self.status_code = status_code + self.reason = reason + self.headers = headers + self.content = content + self.timestamp_start = timestamp_start + self.timestamp_end = timestamp_end + + +class Response(Message): + """ + An HTTP response. + """ + def __init__(self, *args, **kwargs): + data = ResponseData(*args, **kwargs) + super(Response, self).__init__(data) + + def __repr__(self): + if self.content: + details = "{}, {}".format( + self.headers.get("content-type", "unknown content type"), + utils.pretty_size(len(self.content)) + ) + else: + details = "no content" + return "Response({status_code} {reason}, {details})".format( + status_code=self.status_code, + reason=self.reason, + details=details + ) + + @property + def status_code(self): + """ + HTTP Status Code, e.g. ``200``. + """ + return self.data.status_code + + @status_code.setter + def status_code(self, status_code): + self.data.status_code = status_code + + @property + def reason(self): + """ + HTTP Reason Phrase, e.g. "Not Found". + This is always :py:obj:`None` for HTTP2 requests, because HTTP2 responses do not contain a reason phrase. + """ + return _native(self.data.reason) + + @reason.setter + def reason(self, reason): + self.data.reason = _always_bytes(reason) + + @property + def cookies(self): + """ + Get the contents of all Set-Cookie headers. + + A possibly empty :py:class:`ODict`, where keys are cookie name strings, + and values are [value, attr] lists. Value is a string, and attr is + an ODictCaseless containing cookie attributes. Within attrs, unary + attributes (e.g. HTTPOnly) are indicated by a Null value. + """ + ret = [] + for header in self.headers.get_all("set-cookie"): + v = cookies.parse_set_cookie_header(header) + if v: + name, value, attrs = v + ret.append([name, [value, attrs]]) + return ODict(ret) + + @cookies.setter + def cookies(self, odict): + values = [] + for i in odict.lst: + header = cookies.format_set_cookie_header(i[0], i[1][0], i[1][1]) + values.append(header) + self.headers.set_all("set-cookie", values) + + # Legacy + + def get_cookies(self): # pragma: nocover + warnings.warn(".get_cookies is deprecated, use .cookies instead.", DeprecationWarning) + return self.cookies + + def set_cookies(self, odict): # pragma: nocover + warnings.warn(".set_cookies is deprecated, use .cookies instead.", DeprecationWarning) + self.cookies = odict + + @property + def msg(self): # pragma: nocover + warnings.warn(".msg is deprecated, use .reason instead.", DeprecationWarning) + return self.reason + + @msg.setter + def msg(self, reason): # pragma: nocover + warnings.warn(".msg is deprecated, use .reason instead.", DeprecationWarning) + self.reason = reason diff --git a/netlib/http/status_codes.py b/netlib/http/status_codes.py new file mode 100644 index 00000000..8a4dc1f5 --- /dev/null +++ b/netlib/http/status_codes.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import, print_function, division + +CONTINUE = 100 +SWITCHING = 101 +OK = 200 +CREATED = 201 +ACCEPTED = 202 +NON_AUTHORITATIVE_INFORMATION = 203 +NO_CONTENT = 204 +RESET_CONTENT = 205 +PARTIAL_CONTENT = 206 +MULTI_STATUS = 207 + +MULTIPLE_CHOICE = 300 +MOVED_PERMANENTLY = 301 +FOUND = 302 +SEE_OTHER = 303 +NOT_MODIFIED = 304 +USE_PROXY = 305 +TEMPORARY_REDIRECT = 307 + +BAD_REQUEST = 400 +UNAUTHORIZED = 401 +PAYMENT_REQUIRED = 402 +FORBIDDEN = 403 +NOT_FOUND = 404 +NOT_ALLOWED = 405 +NOT_ACCEPTABLE = 406 +PROXY_AUTH_REQUIRED = 407 +REQUEST_TIMEOUT = 408 +CONFLICT = 409 +GONE = 410 +LENGTH_REQUIRED = 411 +PRECONDITION_FAILED = 412 +REQUEST_ENTITY_TOO_LARGE = 413 +REQUEST_URI_TOO_LONG = 414 +UNSUPPORTED_MEDIA_TYPE = 415 +REQUESTED_RANGE_NOT_SATISFIABLE = 416 +EXPECTATION_FAILED = 417 +IM_A_TEAPOT = 418 + +INTERNAL_SERVER_ERROR = 500 +NOT_IMPLEMENTED = 501 +BAD_GATEWAY = 502 +SERVICE_UNAVAILABLE = 503 +GATEWAY_TIMEOUT = 504 +HTTP_VERSION_NOT_SUPPORTED = 505 +INSUFFICIENT_STORAGE_SPACE = 507 +NOT_EXTENDED = 510 + +RESPONSES = { + # 100 + CONTINUE: "Continue", + SWITCHING: "Switching Protocols", + + # 200 + OK: "OK", + CREATED: "Created", + ACCEPTED: "Accepted", + NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information", + NO_CONTENT: "No Content", + RESET_CONTENT: "Reset Content.", + PARTIAL_CONTENT: "Partial Content", + MULTI_STATUS: "Multi-Status", + + # 300 + MULTIPLE_CHOICE: "Multiple Choices", + MOVED_PERMANENTLY: "Moved Permanently", + FOUND: "Found", + SEE_OTHER: "See Other", + NOT_MODIFIED: "Not Modified", + USE_PROXY: "Use Proxy", + # 306 not defined?? + TEMPORARY_REDIRECT: "Temporary Redirect", + + # 400 + BAD_REQUEST: "Bad Request", + UNAUTHORIZED: "Unauthorized", + PAYMENT_REQUIRED: "Payment Required", + FORBIDDEN: "Forbidden", + NOT_FOUND: "Not Found", + NOT_ALLOWED: "Method Not Allowed", + NOT_ACCEPTABLE: "Not Acceptable", + PROXY_AUTH_REQUIRED: "Proxy Authentication Required", + REQUEST_TIMEOUT: "Request Time-out", + CONFLICT: "Conflict", + GONE: "Gone", + LENGTH_REQUIRED: "Length Required", + PRECONDITION_FAILED: "Precondition Failed", + REQUEST_ENTITY_TOO_LARGE: "Request Entity Too Large", + REQUEST_URI_TOO_LONG: "Request-URI Too Long", + UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type", + REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable", + EXPECTATION_FAILED: "Expectation Failed", + IM_A_TEAPOT: "I'm a teapot", + + # 500 + INTERNAL_SERVER_ERROR: "Internal Server Error", + NOT_IMPLEMENTED: "Not Implemented", + BAD_GATEWAY: "Bad Gateway", + SERVICE_UNAVAILABLE: "Service Unavailable", + GATEWAY_TIMEOUT: "Gateway Time-out", + HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported", + INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space", + NOT_EXTENDED: "Not Extended" +} diff --git a/netlib/http/user_agents.py b/netlib/http/user_agents.py new file mode 100644 index 00000000..e8681908 --- /dev/null +++ b/netlib/http/user_agents.py @@ -0,0 +1,52 @@ +from __future__ import (absolute_import, print_function, division) + +""" + A small collection of useful user-agent header strings. These should be + kept reasonably current to reflect common usage. +""" + +# pylint: line-too-long + +# A collection of (name, shortcut, string) tuples. + +UASTRINGS = [ + ("android", + "a", + "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Nexus 7 Build/JRO03D) AFL/01.04.02"), # noqa + ("blackberry", + "l", + "Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+"), # noqa + ("bingbot", + "b", + "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"), # noqa + ("chrome", + "c", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"), # noqa + ("firefox", + "f", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1"), # noqa + ("googlebot", + "g", + "Googlebot/2.1 (+http://www.googlebot.com/bot.html)"), # noqa + ("ie9", + "i", + "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US)"), # noqa + ("ipad", + "p", + "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3"), # noqa + ("iphone", + "h", + "Mozilla/5.0 (iPhone; CPU iPhone OS 4_2_1 like Mac OS X) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5"), # noqa + ("safari", + "s", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10"), # noqa +] + + +def get_by_shortcut(s): + """ + Retrieve a user agent entry by shortcut. + """ + for i in UASTRINGS: + if s == i[1]: + return i diff --git a/netlib/netlib/__init__.py b/netlib/netlib/__init__.py deleted file mode 100644 index 9b4faa33..00000000 --- a/netlib/netlib/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from __future__ import (absolute_import, print_function, division) diff --git a/netlib/netlib/certutils.py b/netlib/netlib/certutils.py deleted file mode 100644 index 616a778e..00000000 --- a/netlib/netlib/certutils.py +++ /dev/null @@ -1,472 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -import os -import ssl -import time -import datetime -from six.moves import filter -import ipaddress - -import sys -from pyasn1.type import univ, constraint, char, namedtype, tag -from pyasn1.codec.der.decoder import decode -from pyasn1.error import PyAsn1Error -import OpenSSL - -from .utils import Serializable - -# Default expiry must not be too long: https://github.com/mitmproxy/mitmproxy/issues/815 - -DEFAULT_EXP = 94608000 # = 24 * 60 * 60 * 365 * 3 -# Generated with "openssl dhparam". It's too slow to generate this on startup. -DEFAULT_DHPARAM = b""" ------BEGIN DH PARAMETERS----- -MIICCAKCAgEAyT6LzpwVFS3gryIo29J5icvgxCnCebcdSe/NHMkD8dKJf8suFCg3 -O2+dguLakSVif/t6dhImxInJk230HmfC8q93hdcg/j8rLGJYDKu3ik6H//BAHKIv -j5O9yjU3rXCfmVJQic2Nne39sg3CreAepEts2TvYHhVv3TEAzEqCtOuTjgDv0ntJ -Gwpj+BJBRQGG9NvprX1YGJ7WOFBP/hWU7d6tgvE6Xa7T/u9QIKpYHMIkcN/l3ZFB -chZEqVlyrcngtSXCROTPcDOQ6Q8QzhaBJS+Z6rcsd7X+haiQqvoFcmaJ08Ks6LQC -ZIL2EtYJw8V8z7C0igVEBIADZBI6OTbuuhDwRw//zU1uq52Oc48CIZlGxTYG/Evq -o9EWAXUYVzWkDSTeBH1r4z/qLPE2cnhtMxbFxuvK53jGB0emy2y1Ei6IhKshJ5qX -IB/aE7SSHyQ3MDHHkCmQJCsOd4Mo26YX61NZ+n501XjqpCBQ2+DfZCBh8Va2wDyv -A2Ryg9SUz8j0AXViRNMJgJrr446yro/FuJZwnQcO3WQnXeqSBnURqKjmqkeFP+d8 -6mk2tqJaY507lRNqtGlLnj7f5RNoBFJDCLBNurVgfvq9TCVWKDIFD4vZRjCrnl6I -rD693XKIHUCWOjMh1if6omGXKHH40QuME2gNa50+YPn1iYDl88uDbbMCAQI= ------END DH PARAMETERS----- -""" - - -def create_ca(o, cn, exp): - key = OpenSSL.crypto.PKey() - key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) - cert = OpenSSL.crypto.X509() - cert.set_serial_number(int(time.time() * 10000)) - cert.set_version(2) - cert.get_subject().CN = cn - cert.get_subject().O = o - cert.gmtime_adj_notBefore(-3600 * 48) - cert.gmtime_adj_notAfter(exp) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(key) - cert.add_extensions([ - OpenSSL.crypto.X509Extension( - b"basicConstraints", - True, - b"CA:TRUE" - ), - OpenSSL.crypto.X509Extension( - b"nsCertType", - False, - b"sslCA" - ), - OpenSSL.crypto.X509Extension( - b"extendedKeyUsage", - False, - b"serverAuth,clientAuth,emailProtection,timeStamping,msCodeInd,msCodeCom,msCTLSign,msSGC,msEFS,nsSGC" - ), - OpenSSL.crypto.X509Extension( - b"keyUsage", - True, - b"keyCertSign, cRLSign" - ), - OpenSSL.crypto.X509Extension( - b"subjectKeyIdentifier", - False, - b"hash", - subject=cert - ), - ]) - cert.sign(key, "sha256") - return key, cert - - -def dummy_cert(privkey, cacert, commonname, sans): - """ - Generates a dummy certificate. - - privkey: CA private key - cacert: CA certificate - commonname: Common name for the generated certificate. - sans: A list of Subject Alternate Names. - - Returns cert if operation succeeded, None if not. - """ - ss = [] - for i in sans: - try: - ipaddress.ip_address(i.decode("ascii")) - except ValueError: - ss.append(b"DNS: %s" % i) - else: - ss.append(b"IP: %s" % i) - ss = b", ".join(ss) - - cert = OpenSSL.crypto.X509() - cert.gmtime_adj_notBefore(-3600 * 48) - cert.gmtime_adj_notAfter(DEFAULT_EXP) - cert.set_issuer(cacert.get_subject()) - if commonname is not None: - cert.get_subject().CN = commonname - cert.set_serial_number(int(time.time() * 10000)) - if ss: - cert.set_version(2) - cert.add_extensions( - [OpenSSL.crypto.X509Extension(b"subjectAltName", False, ss)]) - cert.set_pubkey(cacert.get_pubkey()) - cert.sign(privkey, "sha256") - return SSLCert(cert) - - -# DNTree did not pass TestCertStore.test_sans_change and is temporarily replaced by a simple dict. -# -# class _Node(UserDict.UserDict): -# def __init__(self): -# UserDict.UserDict.__init__(self) -# self.value = None -# -# -# class DNTree: -# """ -# Domain store that knows about wildcards. DNS wildcards are very -# restricted - the only valid variety is an asterisk on the left-most -# domain component, i.e.: -# -# *.foo.com -# """ -# def __init__(self): -# self.d = _Node() -# -# def add(self, dn, cert): -# parts = dn.split(".") -# parts.reverse() -# current = self.d -# for i in parts: -# current = current.setdefault(i, _Node()) -# current.value = cert -# -# def get(self, dn): -# parts = dn.split(".") -# current = self.d -# for i in reversed(parts): -# if i in current: -# current = current[i] -# elif "*" in current: -# return current["*"].value -# else: -# return None -# return current.value - - -class CertStoreEntry(object): - - def __init__(self, cert, privatekey, chain_file): - self.cert = cert - self.privatekey = privatekey - self.chain_file = chain_file - - -class CertStore(object): - - """ - Implements an in-memory certificate store. - """ - - def __init__( - self, - default_privatekey, - default_ca, - default_chain_file, - dhparams): - self.default_privatekey = default_privatekey - self.default_ca = default_ca - self.default_chain_file = default_chain_file - self.dhparams = dhparams - self.certs = dict() - - @staticmethod - def load_dhparam(path): - - # netlib<=0.10 doesn't generate a dhparam file. - # Create it now if neccessary. - if not os.path.exists(path): - with open(path, "wb") as f: - f.write(DEFAULT_DHPARAM) - - bio = OpenSSL.SSL._lib.BIO_new_file(path.encode(sys.getfilesystemencoding()), b"r") - if bio != OpenSSL.SSL._ffi.NULL: - bio = OpenSSL.SSL._ffi.gc(bio, OpenSSL.SSL._lib.BIO_free) - dh = OpenSSL.SSL._lib.PEM_read_bio_DHparams( - bio, - OpenSSL.SSL._ffi.NULL, - OpenSSL.SSL._ffi.NULL, - OpenSSL.SSL._ffi.NULL) - dh = OpenSSL.SSL._ffi.gc(dh, OpenSSL.SSL._lib.DH_free) - return dh - - @classmethod - def from_store(cls, path, basename): - ca_path = os.path.join(path, basename + "-ca.pem") - if not os.path.exists(ca_path): - key, ca = cls.create_store(path, basename) - else: - with open(ca_path, "rb") as f: - raw = f.read() - ca = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, - raw) - key = OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, - raw) - dh_path = os.path.join(path, basename + "-dhparam.pem") - dh = cls.load_dhparam(dh_path) - return cls(key, ca, ca_path, dh) - - @staticmethod - def create_store(path, basename, o=None, cn=None, expiry=DEFAULT_EXP): - if not os.path.exists(path): - os.makedirs(path) - - o = o or basename - cn = cn or basename - - key, ca = create_ca(o=o, cn=cn, exp=expiry) - # Dump the CA plus private key - with open(os.path.join(path, basename + "-ca.pem"), "wb") as f: - f.write( - OpenSSL.crypto.dump_privatekey( - OpenSSL.crypto.FILETYPE_PEM, - key)) - f.write( - OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, - ca)) - - # Dump the certificate in PEM format - with open(os.path.join(path, basename + "-ca-cert.pem"), "wb") as f: - f.write( - OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, - ca)) - - # Create a .cer file with the same contents for Android - with open(os.path.join(path, basename + "-ca-cert.cer"), "wb") as f: - f.write( - OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, - ca)) - - # Dump the certificate in PKCS12 format for Windows devices - with open(os.path.join(path, basename + "-ca-cert.p12"), "wb") as f: - p12 = OpenSSL.crypto.PKCS12() - p12.set_certificate(ca) - p12.set_privatekey(key) - f.write(p12.export()) - - with open(os.path.join(path, basename + "-dhparam.pem"), "wb") as f: - f.write(DEFAULT_DHPARAM) - - return key, ca - - def add_cert_file(self, spec, path): - with open(path, "rb") as f: - raw = f.read() - cert = SSLCert( - OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, - raw)) - try: - privatekey = OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, - raw) - except Exception: - privatekey = self.default_privatekey - self.add_cert( - CertStoreEntry(cert, privatekey, path), - spec - ) - - def add_cert(self, entry, *names): - """ - Adds a cert to the certstore. We register the CN in the cert plus - any SANs, and also the list of names provided as an argument. - """ - if entry.cert.cn: - self.certs[entry.cert.cn] = entry - for i in entry.cert.altnames: - self.certs[i] = entry - for i in names: - self.certs[i] = entry - - @staticmethod - def asterisk_forms(dn): - if dn is None: - return [] - parts = dn.split(b".") - parts.reverse() - curr_dn = b"" - dn_forms = [b"*"] - for part in parts[:-1]: - curr_dn = b"." + part + curr_dn # .example.com - dn_forms.append(b"*" + curr_dn) # *.example.com - if parts[-1] != b"*": - dn_forms.append(parts[-1] + curr_dn) - return dn_forms - - def get_cert(self, commonname, sans): - """ - Returns an (cert, privkey, cert_chain) tuple. - - commonname: Common name for the generated certificate. Must be a - valid, plain-ASCII, IDNA-encoded domain name. - - sans: A list of Subject Alternate Names. - """ - - potential_keys = self.asterisk_forms(commonname) - for s in sans: - potential_keys.extend(self.asterisk_forms(s)) - potential_keys.append((commonname, tuple(sans))) - - name = next( - filter(lambda key: key in self.certs, potential_keys), - None - ) - if name: - entry = self.certs[name] - else: - entry = CertStoreEntry( - cert=dummy_cert( - self.default_privatekey, - self.default_ca, - commonname, - sans), - privatekey=self.default_privatekey, - chain_file=self.default_chain_file) - self.certs[(commonname, tuple(sans))] = entry - - return entry.cert, entry.privatekey, entry.chain_file - - -class _GeneralName(univ.Choice): - # We are only interested in dNSNames. We use a default handler to ignore - # other types. - # TODO: We should also handle iPAddresses. - componentType = namedtype.NamedTypes( - namedtype.NamedType('dNSName', char.IA5String().subtype( - implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2) - ) - ), - ) - - -class _GeneralNames(univ.SequenceOf): - componentType = _GeneralName() - sizeSpec = univ.SequenceOf.sizeSpec + \ - constraint.ValueSizeConstraint(1, 1024) - - -class SSLCert(Serializable): - - def __init__(self, cert): - """ - Returns a (common name, [subject alternative names]) tuple. - """ - self.x509 = cert - - def __eq__(self, other): - return self.digest("sha256") == other.digest("sha256") - - def __ne__(self, other): - return not self.__eq__(other) - - def get_state(self): - return self.to_pem() - - def set_state(self, state): - self.x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, state) - - @classmethod - def from_state(cls, state): - cls.from_pem(state) - - @classmethod - def from_pem(cls, txt): - x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, txt) - return cls(x509) - - @classmethod - def from_der(cls, der): - pem = ssl.DER_cert_to_PEM_cert(der) - return cls.from_pem(pem) - - def to_pem(self): - return OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, - self.x509) - - def digest(self, name): - return self.x509.digest(name) - - @property - def issuer(self): - return self.x509.get_issuer().get_components() - - @property - def notbefore(self): - t = self.x509.get_notBefore() - return datetime.datetime.strptime(t.decode("ascii"), "%Y%m%d%H%M%SZ") - - @property - def notafter(self): - t = self.x509.get_notAfter() - return datetime.datetime.strptime(t.decode("ascii"), "%Y%m%d%H%M%SZ") - - @property - def has_expired(self): - return self.x509.has_expired() - - @property - def subject(self): - return self.x509.get_subject().get_components() - - @property - def serial(self): - return self.x509.get_serial_number() - - @property - def keyinfo(self): - pk = self.x509.get_pubkey() - types = { - OpenSSL.crypto.TYPE_RSA: "RSA", - OpenSSL.crypto.TYPE_DSA: "DSA", - } - return ( - types.get(pk.type(), "UNKNOWN"), - pk.bits() - ) - - @property - def cn(self): - c = None - for i in self.subject: - if i[0] == b"CN": - c = i[1] - return c - - @property - def altnames(self): - """ - Returns: - All DNS altnames. - """ - # tcp.TCPClient.convert_to_ssl assumes that this property only contains DNS altnames for hostname verification. - altnames = [] - for i in range(self.x509.get_extension_count()): - ext = self.x509.get_extension(i) - if ext.get_short_name() == b"subjectAltName": - try: - dec = decode(ext.get_data(), asn1Spec=_GeneralNames()) - except PyAsn1Error: - continue - for i in dec[0]: - altnames.append(i[0].asOctets()) - return altnames diff --git a/netlib/netlib/encoding.py b/netlib/netlib/encoding.py deleted file mode 100644 index 14479e00..00000000 --- a/netlib/netlib/encoding.py +++ /dev/null @@ -1,88 +0,0 @@ -""" - Utility functions for decoding response bodies. -""" -from __future__ import absolute_import -from io import BytesIO -import gzip -import zlib -from .utils import always_byte_args - - -ENCODINGS = {"identity", "gzip", "deflate"} - - -def decode(e, content): - if not isinstance(content, bytes): - return None - encoding_map = { - "identity": identity, - "gzip": decode_gzip, - "deflate": decode_deflate, - } - if e not in encoding_map: - return None - return encoding_map[e](content) - - -def encode(e, content): - if not isinstance(content, bytes): - return None - encoding_map = { - "identity": identity, - "gzip": encode_gzip, - "deflate": encode_deflate, - } - if e not in encoding_map: - return None - return encoding_map[e](content) - - -def identity(content): - """ - Returns content unchanged. Identity is the default value of - Accept-Encoding headers. - """ - return content - - -def decode_gzip(content): - gfile = gzip.GzipFile(fileobj=BytesIO(content)) - try: - return gfile.read() - except (IOError, EOFError): - return None - - -def encode_gzip(content): - s = BytesIO() - gf = gzip.GzipFile(fileobj=s, mode='wb') - gf.write(content) - gf.close() - return s.getvalue() - - -def decode_deflate(content): - """ - Returns decompressed data for DEFLATE. Some servers may respond with - compressed data without a zlib header or checksum. An undocumented - feature of zlib permits the lenient decompression of data missing both - values. - - http://bugs.python.org/issue5784 - """ - try: - try: - return zlib.decompress(content) - except zlib.error: - return zlib.decompress(content, -15) - except zlib.error: - return None - - -def encode_deflate(content): - """ - Returns compressed content, always including zlib header and checksum. - """ - return zlib.compress(content) - -__all__ = ["ENCODINGS", "encode", "decode"] diff --git a/netlib/netlib/exceptions.py b/netlib/netlib/exceptions.py deleted file mode 100644 index 05f1054b..00000000 --- a/netlib/netlib/exceptions.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -We try to be very hygienic regarding the exceptions we throw: -Every Exception netlib raises shall be a subclass of NetlibException. - - -See also: http://lucumr.pocoo.org/2014/10/16/on-error-handling/ -""" -from __future__ import absolute_import, print_function, division - - -class NetlibException(Exception): - """ - Base class for all exceptions thrown by netlib. - """ - def __init__(self, message=None): - super(NetlibException, self).__init__(message) - - -class Disconnect(object): - """Immediate EOF""" - - -class HttpException(NetlibException): - pass - - -class HttpReadDisconnect(HttpException, Disconnect): - pass - - -class HttpSyntaxException(HttpException): - pass - - -class TcpException(NetlibException): - pass - - -class TcpDisconnect(TcpException, Disconnect): - pass - - -class TcpReadIncomplete(TcpException): - pass - - -class TcpTimeout(TcpException): - pass - - -class TlsException(NetlibException): - pass - - -class InvalidCertificateException(TlsException): - pass diff --git a/netlib/netlib/http/__init__.py b/netlib/netlib/http/__init__.py deleted file mode 100644 index fd632cd5..00000000 --- a/netlib/netlib/http/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import absolute_import, print_function, division -from .request import Request -from .response import Response -from .headers import Headers -from .message import decoded, CONTENT_MISSING -from . import http1, http2 - -__all__ = [ - "Request", - "Response", - "Headers", - "decoded", "CONTENT_MISSING", - "http1", "http2", -] diff --git a/netlib/netlib/http/authentication.py b/netlib/netlib/http/authentication.py deleted file mode 100644 index d769abe5..00000000 --- a/netlib/netlib/http/authentication.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -from argparse import Action, ArgumentTypeError -import binascii - - -def parse_http_basic_auth(s): - words = s.split() - if len(words) != 2: - return None - scheme = words[0] - try: - user = binascii.a2b_base64(words[1]).decode("utf8", "replace") - except binascii.Error: - return None - parts = user.split(':') - if len(parts) != 2: - return None - return scheme, parts[0], parts[1] - - -def assemble_http_basic_auth(scheme, username, password): - v = binascii.b2a_base64((username + ":" + password).encode("utf8")).decode("ascii") - return scheme + " " + v - - -class NullProxyAuth(object): - - """ - No proxy auth at all (returns empty challange headers) - """ - - def __init__(self, password_manager): - self.password_manager = password_manager - - def clean(self, headers_): - """ - Clean up authentication headers, so they're not passed upstream. - """ - - def authenticate(self, headers_): - """ - Tests that the user is allowed to use the proxy - """ - return True - - def auth_challenge_headers(self): - """ - Returns a dictionary containing the headers require to challenge the user - """ - return {} - - -class BasicProxyAuth(NullProxyAuth): - CHALLENGE_HEADER = 'Proxy-Authenticate' - AUTH_HEADER = 'Proxy-Authorization' - - def __init__(self, password_manager, realm): - NullProxyAuth.__init__(self, password_manager) - self.realm = realm - - def clean(self, headers): - del headers[self.AUTH_HEADER] - - def authenticate(self, headers): - auth_value = headers.get(self.AUTH_HEADER) - if not auth_value: - return False - parts = parse_http_basic_auth(auth_value) - if not parts: - return False - scheme, username, password = parts - if scheme.lower() != 'basic': - return False - if not self.password_manager.test(username, password): - return False - self.username = username - return True - - def auth_challenge_headers(self): - return {self.CHALLENGE_HEADER: 'Basic realm="%s"' % self.realm} - - -class PassMan(object): - - def test(self, username_, password_token_): - return False - - -class PassManNonAnon(PassMan): - - """ - Ensure the user specifies a username, accept any password. - """ - - def test(self, username, password_token_): - if username: - return True - return False - - -class PassManHtpasswd(PassMan): - - """ - Read usernames and passwords from an htpasswd file - """ - - def __init__(self, path): - """ - Raises ValueError if htpasswd file is invalid. - """ - import passlib.apache - self.htpasswd = passlib.apache.HtpasswdFile(path) - - def test(self, username, password_token): - return bool(self.htpasswd.check_password(username, password_token)) - - -class PassManSingleUser(PassMan): - - def __init__(self, username, password): - self.username, self.password = username, password - - def test(self, username, password_token): - return self.username == username and self.password == password_token - - -class AuthAction(Action): - - """ - Helper class to allow seamless integration int argparse. Example usage: - parser.add_argument( - "--nonanonymous", - action=NonanonymousAuthAction, nargs=0, - help="Allow access to any user long as a credentials are specified." - ) - """ - - def __call__(self, parser, namespace, values, option_string=None): - passman = self.getPasswordManager(values) - authenticator = BasicProxyAuth(passman, "mitmproxy") - setattr(namespace, self.dest, authenticator) - - def getPasswordManager(self, s): # pragma: nocover - raise NotImplementedError() - - -class SingleuserAuthAction(AuthAction): - - def getPasswordManager(self, s): - if len(s.split(':')) != 2: - raise ArgumentTypeError( - "Invalid single-user specification. Please use the format username:password" - ) - username, password = s.split(':') - return PassManSingleUser(username, password) - - -class NonanonymousAuthAction(AuthAction): - - def getPasswordManager(self, s): - return PassManNonAnon() - - -class HtpasswdAuthAction(AuthAction): - - def getPasswordManager(self, s): - return PassManHtpasswd(s) diff --git a/netlib/netlib/http/cookies.py b/netlib/netlib/http/cookies.py deleted file mode 100644 index 18544b5e..00000000 --- a/netlib/netlib/http/cookies.py +++ /dev/null @@ -1,193 +0,0 @@ -import re - -from .. import odict - -""" -A flexible module for cookie parsing and manipulation. - -This module differs from usual standards-compliant cookie modules in a number -of ways. We try to be as permissive as possible, and to retain even mal-formed -information. Duplicate cookies are preserved in parsing, and can be set in -formatting. We do attempt to escape and quote values where needed, but will not -reject data that violate the specs. - -Parsing accepts the formats in RFC6265 and partially RFC2109 and RFC2965. We do -not parse the comma-separated variant of Set-Cookie that allows multiple -cookies to be set in a single header. Technically this should be feasible, but -it turns out that violations of RFC6265 that makes the parsing problem -indeterminate are much more common than genuine occurences of the multi-cookie -variants. Serialization follows RFC6265. - - http://tools.ietf.org/html/rfc6265 - http://tools.ietf.org/html/rfc2109 - http://tools.ietf.org/html/rfc2965 -""" - -# TODO: Disallow LHS-only Cookie values - - -def _read_until(s, start, term): - """ - Read until one of the characters in term is reached. - """ - if start == len(s): - return "", start + 1 - for i in range(start, len(s)): - if s[i] in term: - return s[start:i], i - return s[start:i + 1], i + 1 - - -def _read_token(s, start): - """ - Read a token - the LHS of a token/value pair in a cookie. - """ - return _read_until(s, start, ";=") - - -def _read_quoted_string(s, start): - """ - start: offset to the first quote of the string to be read - - A sort of loose super-set of the various quoted string specifications. - - RFC6265 disallows backslashes or double quotes within quoted strings. - Prior RFCs use backslashes to escape. This leaves us free to apply - backslash escaping by default and be compatible with everything. - """ - escaping = False - ret = [] - # Skip the first quote - i = start # initialize in case the loop doesn't run. - for i in range(start + 1, len(s)): - if escaping: - ret.append(s[i]) - escaping = False - elif s[i] == '"': - break - elif s[i] == "\\": - escaping = True - else: - ret.append(s[i]) - return "".join(ret), i + 1 - - -def _read_value(s, start, delims): - """ - Reads a value - the RHS of a token/value pair in a cookie. - - special: If the value is special, commas are premitted. Else comma - terminates. This helps us support old and new style values. - """ - if start >= len(s): - return "", start - elif s[start] == '"': - return _read_quoted_string(s, start) - else: - return _read_until(s, start, delims) - - -def _read_pairs(s, off=0): - """ - Read pairs of lhs=rhs values. - - off: start offset - specials: a lower-cased list of keys that may contain commas - """ - vals = [] - while True: - lhs, off = _read_token(s, off) - lhs = lhs.lstrip() - if lhs: - rhs = None - if off < len(s): - if s[off] == "=": - rhs, off = _read_value(s, off + 1, ";") - vals.append([lhs, rhs]) - off += 1 - if not off < len(s): - break - return vals, off - - -def _has_special(s): - for i in s: - if i in '",;\\': - return True - o = ord(i) - if o < 0x21 or o > 0x7e: - return True - return False - - -ESCAPE = re.compile(r"([\"\\])") - - -def _format_pairs(lst, specials=(), sep="; "): - """ - specials: A lower-cased list of keys that will not be quoted. - """ - vals = [] - for k, v in lst: - if v is None: - vals.append(k) - else: - if k.lower() not in specials and _has_special(v): - v = ESCAPE.sub(r"\\\1", v) - v = '"%s"' % v - vals.append("%s=%s" % (k, v)) - return sep.join(vals) - - -def _format_set_cookie_pairs(lst): - return _format_pairs( - lst, - specials=("expires", "path") - ) - - -def _parse_set_cookie_pairs(s): - """ - For Set-Cookie, we support multiple cookies as described in RFC2109. - This function therefore returns a list of lists. - """ - pairs, off_ = _read_pairs(s) - return pairs - - -def parse_set_cookie_header(line): - """ - Parse a Set-Cookie header value - - Returns a (name, value, attrs) tuple, or None, where attrs is an - ODictCaseless set of attributes. No attempt is made to parse attribute - values - they are treated purely as strings. - """ - pairs = _parse_set_cookie_pairs(line) - if pairs: - return pairs[0][0], pairs[0][1], odict.ODictCaseless(pairs[1:]) - - -def format_set_cookie_header(name, value, attrs): - """ - Formats a Set-Cookie header value. - """ - pairs = [[name, value]] - pairs.extend(attrs.lst) - return _format_set_cookie_pairs(pairs) - - -def parse_cookie_header(line): - """ - Parse a Cookie header value. - Returns a (possibly empty) ODict object. - """ - pairs, off_ = _read_pairs(line) - return odict.ODict(pairs) - - -def format_cookie_header(od): - """ - Formats a Cookie header value. - """ - return _format_pairs(od.lst) diff --git a/netlib/netlib/http/headers.py b/netlib/netlib/http/headers.py deleted file mode 100644 index 78404796..00000000 --- a/netlib/netlib/http/headers.py +++ /dev/null @@ -1,204 +0,0 @@ -""" - -Unicode Handling ----------------- -See also: http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/ -""" -from __future__ import absolute_import, print_function, division -import copy -try: - from collections.abc import MutableMapping -except ImportError: # pragma: nocover - from collections import MutableMapping # Workaround for Python < 3.3 - - -import six - -from netlib.utils import always_byte_args, always_bytes, Serializable - -if six.PY2: # pragma: nocover - _native = lambda x: x - _always_bytes = lambda x: x - _always_byte_args = lambda x: x -else: - # While headers _should_ be ASCII, it's not uncommon for certain headers to be utf-8 encoded. - _native = lambda x: x.decode("utf-8", "surrogateescape") - _always_bytes = lambda x: always_bytes(x, "utf-8", "surrogateescape") - _always_byte_args = always_byte_args("utf-8", "surrogateescape") - - -class Headers(MutableMapping, Serializable): - """ - Header class which allows both convenient access to individual headers as well as - direct access to the underlying raw data. Provides a full dictionary interface. - - Example: - - .. code-block:: python - - # Create headers with keyword arguments - >>> h = Headers(host="example.com", content_type="application/xml") - - # Headers mostly behave like a normal dict. - >>> h["Host"] - "example.com" - - # HTTP Headers are case insensitive - >>> h["host"] - "example.com" - - # Headers can also be creatd from a list of raw (header_name, header_value) byte tuples - >>> h = Headers([ - [b"Host",b"example.com"], - [b"Accept",b"text/html"], - [b"accept",b"application/xml"] - ]) - - # Multiple headers are folded into a single header as per RFC7230 - >>> h["Accept"] - "text/html, application/xml" - - # Setting a header removes all existing headers with the same name. - >>> h["Accept"] = "application/text" - >>> h["Accept"] - "application/text" - - # bytes(h) returns a HTTP1 header block. - >>> print(bytes(h)) - Host: example.com - Accept: application/text - - # For full control, the raw header fields can be accessed - >>> h.fields - - Caveats: - For use with the "Set-Cookie" header, see :py:meth:`get_all`. - """ - - @_always_byte_args - def __init__(self, fields=None, **headers): - """ - Args: - fields: (optional) list of ``(name, value)`` header byte tuples, - e.g. ``[(b"Host", b"example.com")]``. All names and values must be bytes. - **headers: Additional headers to set. Will overwrite existing values from `fields`. - For convenience, underscores in header names will be transformed to dashes - - this behaviour does not extend to other methods. - If ``**headers`` contains multiple keys that have equal ``.lower()`` s, - the behavior is undefined. - """ - self.fields = fields or [] - - for name, value in self.fields: - if not isinstance(name, bytes) or not isinstance(value, bytes): - raise ValueError("Headers passed as fields must be bytes.") - - # content_type -> content-type - headers = { - _always_bytes(name).replace(b"_", b"-"): value - for name, value in six.iteritems(headers) - } - self.update(headers) - - def __bytes__(self): - if self.fields: - return b"\r\n".join(b": ".join(field) for field in self.fields) + b"\r\n" - else: - return b"" - - if six.PY2: # pragma: nocover - __str__ = __bytes__ - - @_always_byte_args - def __getitem__(self, name): - values = self.get_all(name) - if not values: - raise KeyError(name) - return ", ".join(values) - - @_always_byte_args - def __setitem__(self, name, value): - idx = self._index(name) - - # To please the human eye, we insert at the same position the first existing header occured. - if idx is not None: - del self[name] - self.fields.insert(idx, [name, value]) - else: - self.fields.append([name, value]) - - @_always_byte_args - def __delitem__(self, name): - if name not in self: - raise KeyError(name) - name = name.lower() - self.fields = [ - field for field in self.fields - if name != field[0].lower() - ] - - def __iter__(self): - seen = set() - for name, _ in self.fields: - name_lower = name.lower() - if name_lower not in seen: - seen.add(name_lower) - yield _native(name) - - def __len__(self): - return len(set(name.lower() for name, _ in self.fields)) - - # __hash__ = object.__hash__ - - def _index(self, name): - name = name.lower() - for i, field in enumerate(self.fields): - if field[0].lower() == name: - return i - return None - - def __eq__(self, other): - if isinstance(other, Headers): - return self.fields == other.fields - return False - - def __ne__(self, other): - return not self.__eq__(other) - - @_always_byte_args - def get_all(self, name): - """ - Like :py:meth:`get`, but does not fold multiple headers into a single one. - This is useful for Set-Cookie headers, which do not support folding. - - See also: https://tools.ietf.org/html/rfc7230#section-3.2.2 - """ - name_lower = name.lower() - values = [_native(value) for n, value in self.fields if n.lower() == name_lower] - return values - - @_always_byte_args - def set_all(self, name, values): - """ - Explicitly set multiple headers for the given key. - See: :py:meth:`get_all` - """ - values = map(_always_bytes, values) # _always_byte_args does not fix lists - if name in self: - del self[name] - self.fields.extend( - [name, value] for value in values - ) - - def copy(self): - return Headers(copy.copy(self.fields)) - - def get_state(self): - return tuple(tuple(field) for field in self.fields) - - def set_state(self, state): - self.fields = [list(field) for field in state] - - @classmethod - def from_state(cls, state): - return cls([list(field) for field in state]) \ No newline at end of file diff --git a/netlib/netlib/http/http1/__init__.py b/netlib/netlib/http/http1/__init__.py deleted file mode 100644 index 2aa7e26a..00000000 --- a/netlib/netlib/http/http1/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import absolute_import, print_function, division -from .read import ( - read_request, read_request_head, - read_response, read_response_head, - read_body, - connection_close, - expected_http_body_size, -) -from .assemble import ( - assemble_request, assemble_request_head, - assemble_response, assemble_response_head, - assemble_body, -) - - -__all__ = [ - "read_request", "read_request_head", - "read_response", "read_response_head", - "read_body", - "connection_close", - "expected_http_body_size", - "assemble_request", "assemble_request_head", - "assemble_response", "assemble_response_head", - "assemble_body", -] diff --git a/netlib/netlib/http/http1/assemble.py b/netlib/netlib/http/http1/assemble.py deleted file mode 100644 index 785ee8d3..00000000 --- a/netlib/netlib/http/http1/assemble.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import absolute_import, print_function, division - -from ... import utils -import itertools -from ...exceptions import HttpException -from .. import CONTENT_MISSING - - -def assemble_request(request): - if request.content == CONTENT_MISSING: - raise HttpException("Cannot assemble flow with CONTENT_MISSING") - head = assemble_request_head(request) - body = b"".join(assemble_body(request.data.headers, [request.data.content])) - return head + body - - -def assemble_request_head(request): - first_line = _assemble_request_line(request.data) - headers = _assemble_request_headers(request.data) - return b"%s\r\n%s\r\n" % (first_line, headers) - - -def assemble_response(response): - if response.content == CONTENT_MISSING: - raise HttpException("Cannot assemble flow with CONTENT_MISSING") - head = assemble_response_head(response) - body = b"".join(assemble_body(response.data.headers, [response.data.content])) - return head + body - - -def assemble_response_head(response): - first_line = _assemble_response_line(response.data) - headers = _assemble_response_headers(response.data) - return b"%s\r\n%s\r\n" % (first_line, headers) - - -def assemble_body(headers, body_chunks): - if "chunked" in headers.get("transfer-encoding", "").lower(): - for chunk in body_chunks: - if chunk: - yield b"%x\r\n%s\r\n" % (len(chunk), chunk) - yield b"0\r\n\r\n" - else: - for chunk in body_chunks: - yield chunk - - -def _assemble_request_line(request_data): - """ - Args: - request_data (netlib.http.request.RequestData) - """ - form = request_data.first_line_format - if form == "relative": - return b"%s %s %s" % ( - request_data.method, - request_data.path, - request_data.http_version - ) - elif form == "authority": - return b"%s %s:%d %s" % ( - request_data.method, - request_data.host, - request_data.port, - request_data.http_version - ) - elif form == "absolute": - return b"%s %s://%s:%d%s %s" % ( - request_data.method, - request_data.scheme, - request_data.host, - request_data.port, - request_data.path, - request_data.http_version - ) - else: - raise RuntimeError("Invalid request form") - - -def _assemble_request_headers(request_data): - """ - Args: - request_data (netlib.http.request.RequestData) - """ - headers = request_data.headers.copy() - if "host" not in headers and request_data.scheme and request_data.host and request_data.port: - headers["host"] = utils.hostport( - request_data.scheme, - request_data.host, - request_data.port - ) - return bytes(headers) - - -def _assemble_response_line(response_data): - return b"%s %d %s" % ( - response_data.http_version, - response_data.status_code, - response_data.reason, - ) - - -def _assemble_response_headers(response): - return bytes(response.headers) diff --git a/netlib/netlib/http/http1/read.py b/netlib/netlib/http/http1/read.py deleted file mode 100644 index 6e3a1b93..00000000 --- a/netlib/netlib/http/http1/read.py +++ /dev/null @@ -1,362 +0,0 @@ -from __future__ import absolute_import, print_function, division -import time -import sys -import re - -from ... import utils -from ...exceptions import HttpReadDisconnect, HttpSyntaxException, HttpException, TcpDisconnect -from .. import Request, Response, Headers - - -def read_request(rfile, body_size_limit=None): - request = read_request_head(rfile) - expected_body_size = expected_http_body_size(request) - request.data.content = b"".join(read_body(rfile, expected_body_size, limit=body_size_limit)) - request.timestamp_end = time.time() - return request - - -def read_request_head(rfile): - """ - Parse an HTTP request head (request line + headers) from an input stream - - Args: - rfile: The input stream - - Returns: - The HTTP request object (without body) - - Raises: - HttpReadDisconnect: No bytes can be read from rfile. - HttpSyntaxException: The input is malformed HTTP. - HttpException: Any other error occured. - """ - timestamp_start = time.time() - if hasattr(rfile, "reset_timestamps"): - rfile.reset_timestamps() - - form, method, scheme, host, port, path, http_version = _read_request_line(rfile) - headers = _read_headers(rfile) - - if hasattr(rfile, "first_byte_timestamp"): - # more accurate timestamp_start - timestamp_start = rfile.first_byte_timestamp - - return Request( - form, method, scheme, host, port, path, http_version, headers, None, timestamp_start - ) - - -def read_response(rfile, request, body_size_limit=None): - response = read_response_head(rfile) - expected_body_size = expected_http_body_size(request, response) - response.data.content = b"".join(read_body(rfile, expected_body_size, body_size_limit)) - response.timestamp_end = time.time() - return response - - -def read_response_head(rfile): - """ - Parse an HTTP response head (response line + headers) from an input stream - - Args: - rfile: The input stream - - Returns: - The HTTP request object (without body) - - Raises: - HttpReadDisconnect: No bytes can be read from rfile. - HttpSyntaxException: The input is malformed HTTP. - HttpException: Any other error occured. - """ - - timestamp_start = time.time() - if hasattr(rfile, "reset_timestamps"): - rfile.reset_timestamps() - - http_version, status_code, message = _read_response_line(rfile) - headers = _read_headers(rfile) - - if hasattr(rfile, "first_byte_timestamp"): - # more accurate timestamp_start - timestamp_start = rfile.first_byte_timestamp - - return Response(http_version, status_code, message, headers, None, timestamp_start) - - -def read_body(rfile, expected_size, limit=None, max_chunk_size=4096): - """ - Read an HTTP message body - - Args: - rfile: The input stream - expected_size: The expected body size (see :py:meth:`expected_body_size`) - limit: Maximum body size - max_chunk_size: Maximium chunk size that gets yielded - - Returns: - A generator that yields byte chunks of the content. - - Raises: - HttpException, if an error occurs - - Caveats: - max_chunk_size is not considered if the transfer encoding is chunked. - """ - if not limit or limit < 0: - limit = sys.maxsize - if not max_chunk_size: - max_chunk_size = limit - - if expected_size is None: - for x in _read_chunked(rfile, limit): - yield x - elif expected_size >= 0: - if limit is not None and expected_size > limit: - raise HttpException( - "HTTP Body too large. " - "Limit is {}, content length was advertised as {}".format(limit, expected_size) - ) - bytes_left = expected_size - while bytes_left: - chunk_size = min(bytes_left, max_chunk_size) - content = rfile.read(chunk_size) - if len(content) < chunk_size: - raise HttpException("Unexpected EOF") - yield content - bytes_left -= chunk_size - else: - bytes_left = limit - while bytes_left: - chunk_size = min(bytes_left, max_chunk_size) - content = rfile.read(chunk_size) - if not content: - return - yield content - bytes_left -= chunk_size - not_done = rfile.read(1) - if not_done: - raise HttpException("HTTP body too large. Limit is {}.".format(limit)) - - -def connection_close(http_version, headers): - """ - Checks the message to see if the client connection should be closed - according to RFC 2616 Section 8.1. - """ - # At first, check if we have an explicit Connection header. - if "connection" in headers: - tokens = utils.get_header_tokens(headers, "connection") - if "close" in tokens: - return True - elif "keep-alive" in tokens: - return False - - # If we don't have a Connection header, HTTP 1.1 connections are assumed to - # be persistent - return http_version != "HTTP/1.1" and http_version != b"HTTP/1.1" # FIXME: Remove one case. - - -def expected_http_body_size(request, response=None): - """ - Returns: - The expected body length: - - a positive integer, if the size is known in advance - - None, if the size in unknown in advance (chunked encoding) - - -1, if all data should be read until end of stream. - - Raises: - HttpSyntaxException, if the content length header is invalid - """ - # Determine response size according to - # http://tools.ietf.org/html/rfc7230#section-3.3 - if not response: - headers = request.headers - response_code = None - is_request = True - else: - headers = response.headers - response_code = response.status_code - is_request = False - - if is_request: - if headers.get("expect", "").lower() == "100-continue": - return 0 - else: - if request.method.upper() == "HEAD": - return 0 - if 100 <= response_code <= 199: - return 0 - if response_code == 200 and request.method.upper() == "CONNECT": - return 0 - if response_code in (204, 304): - return 0 - - if "chunked" in headers.get("transfer-encoding", "").lower(): - return None - if "content-length" in headers: - try: - size = int(headers["content-length"]) - if size < 0: - raise ValueError() - return size - except ValueError: - raise HttpSyntaxException("Unparseable Content Length") - if is_request: - return 0 - return -1 - - -def _get_first_line(rfile): - try: - line = rfile.readline() - if line == b"\r\n" or line == b"\n": - # Possible leftover from previous message - line = rfile.readline() - except TcpDisconnect: - raise HttpReadDisconnect("Remote disconnected") - if not line: - raise HttpReadDisconnect("Remote disconnected") - return line.strip() - - -def _read_request_line(rfile): - try: - line = _get_first_line(rfile) - except HttpReadDisconnect: - # We want to provide a better error message. - raise HttpReadDisconnect("Client disconnected") - - try: - method, path, http_version = line.split(b" ") - - if path == b"*" or path.startswith(b"/"): - form = "relative" - scheme, host, port = None, None, None - elif method == b"CONNECT": - form = "authority" - host, port = _parse_authority_form(path) - scheme, path = None, None - else: - form = "absolute" - scheme, host, port, path = utils.parse_url(path) - - _check_http_version(http_version) - except ValueError: - raise HttpSyntaxException("Bad HTTP request line: {}".format(line)) - - return form, method, scheme, host, port, path, http_version - - -def _parse_authority_form(hostport): - """ - Returns (host, port) if hostport is a valid authority-form host specification. - http://tools.ietf.org/html/draft-luotonen-web-proxy-tunneling-01 section 3.1 - - Raises: - ValueError, if the input is malformed - """ - try: - host, port = hostport.split(b":") - port = int(port) - if not utils.is_valid_host(host) or not utils.is_valid_port(port): - raise ValueError() - except ValueError: - raise HttpSyntaxException("Invalid host specification: {}".format(hostport)) - - return host, port - - -def _read_response_line(rfile): - try: - line = _get_first_line(rfile) - except HttpReadDisconnect: - # We want to provide a better error message. - raise HttpReadDisconnect("Server disconnected") - - try: - - parts = line.split(b" ", 2) - if len(parts) == 2: # handle missing message gracefully - parts.append(b"") - - http_version, status_code, message = parts - status_code = int(status_code) - _check_http_version(http_version) - - except ValueError: - raise HttpSyntaxException("Bad HTTP response line: {}".format(line)) - - return http_version, status_code, message - - -def _check_http_version(http_version): - if not re.match(br"^HTTP/\d\.\d$", http_version): - raise HttpSyntaxException("Unknown HTTP version: {}".format(http_version)) - - -def _read_headers(rfile): - """ - Read a set of headers. - Stop once a blank line is reached. - - Returns: - A headers object - - Raises: - HttpSyntaxException - """ - ret = [] - while True: - line = rfile.readline() - if not line or line == b"\r\n" or line == b"\n": - break - if line[0] in b" \t": - if not ret: - raise HttpSyntaxException("Invalid headers") - # continued header - ret[-1][1] = ret[-1][1] + b'\r\n ' + line.strip() - else: - try: - name, value = line.split(b":", 1) - value = value.strip() - if not name: - raise ValueError() - ret.append([name, value]) - except ValueError: - raise HttpSyntaxException("Invalid headers") - return Headers(ret) - - -def _read_chunked(rfile, limit=sys.maxsize): - """ - Read a HTTP body with chunked transfer encoding. - - Args: - rfile: the input file - limit: A positive integer - """ - total = 0 - while True: - line = rfile.readline(128) - if line == b"": - raise HttpException("Connection closed prematurely") - if line != b"\r\n" and line != b"\n": - try: - length = int(line, 16) - except ValueError: - raise HttpSyntaxException("Invalid chunked encoding length: {}".format(line)) - total += length - if total > limit: - raise HttpException( - "HTTP Body too large. Limit is {}, " - "chunked content longer than {}".format(limit, total) - ) - chunk = rfile.read(length) - suffix = rfile.readline(5) - if suffix != b"\r\n": - raise HttpSyntaxException("Malformed chunked body") - if length == 0: - return - yield chunk diff --git a/netlib/netlib/http/http2/__init__.py b/netlib/netlib/http/http2/__init__.py deleted file mode 100644 index 7043d36f..00000000 --- a/netlib/netlib/http/http2/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import absolute_import, print_function, division -from .connections import HTTP2Protocol - -__all__ = [ - "HTTP2Protocol" -] diff --git a/netlib/netlib/http/http2/connections.py b/netlib/netlib/http/http2/connections.py deleted file mode 100644 index 52fa7193..00000000 --- a/netlib/netlib/http/http2/connections.py +++ /dev/null @@ -1,426 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -import itertools -import time - -from hpack.hpack import Encoder, Decoder -from ... import utils -from .. import Headers, Response, Request - -from hyperframe import frame - - -class TCPHandler(object): - - def __init__(self, rfile, wfile=None): - self.rfile = rfile - self.wfile = wfile - - -class HTTP2Protocol(object): - - ERROR_CODES = utils.BiDi( - NO_ERROR=0x0, - PROTOCOL_ERROR=0x1, - INTERNAL_ERROR=0x2, - FLOW_CONTROL_ERROR=0x3, - SETTINGS_TIMEOUT=0x4, - STREAM_CLOSED=0x5, - FRAME_SIZE_ERROR=0x6, - REFUSED_STREAM=0x7, - CANCEL=0x8, - COMPRESSION_ERROR=0x9, - CONNECT_ERROR=0xa, - ENHANCE_YOUR_CALM=0xb, - INADEQUATE_SECURITY=0xc, - HTTP_1_1_REQUIRED=0xd - ) - - CLIENT_CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' - - HTTP2_DEFAULT_SETTINGS = { - frame.SettingsFrame.HEADER_TABLE_SIZE: 4096, - frame.SettingsFrame.ENABLE_PUSH: 1, - frame.SettingsFrame.MAX_CONCURRENT_STREAMS: None, - frame.SettingsFrame.INITIAL_WINDOW_SIZE: 2 ** 16 - 1, - frame.SettingsFrame.MAX_FRAME_SIZE: 2 ** 14, - frame.SettingsFrame.MAX_HEADER_LIST_SIZE: None, - } - - def __init__( - self, - tcp_handler=None, - rfile=None, - wfile=None, - is_server=False, - dump_frames=False, - encoder=None, - decoder=None, - unhandled_frame_cb=None, - ): - self.tcp_handler = tcp_handler or TCPHandler(rfile, wfile) - self.is_server = is_server - self.dump_frames = dump_frames - self.encoder = encoder or Encoder() - self.decoder = decoder or Decoder() - self.unhandled_frame_cb = unhandled_frame_cb - - self.http2_settings = self.HTTP2_DEFAULT_SETTINGS.copy() - self.current_stream_id = None - self.connection_preface_performed = False - - def read_request( - self, - __rfile, - include_body=True, - body_size_limit=None, - allow_empty=False, - ): - if body_size_limit is not None: - raise NotImplementedError() - - self.perform_connection_preface() - - timestamp_start = time.time() - if hasattr(self.tcp_handler.rfile, "reset_timestamps"): - self.tcp_handler.rfile.reset_timestamps() - - stream_id, headers, body = self._receive_transmission( - include_body=include_body, - ) - - if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): - # more accurate timestamp_start - timestamp_start = self.tcp_handler.rfile.first_byte_timestamp - - timestamp_end = time.time() - - authority = headers.get(':authority', b'') - method = headers.get(':method', 'GET') - scheme = headers.get(':scheme', 'https') - path = headers.get(':path', '/') - host = None - port = None - - if path == '*' or path.startswith("/"): - form_in = "relative" - elif method == 'CONNECT': - form_in = "authority" - if ":" in authority: - host, port = authority.split(":", 1) - else: - host = authority - else: - form_in = "absolute" - # FIXME: verify if path or :host contains what we need - scheme, host, port, _ = utils.parse_url(path) - scheme = scheme.decode('ascii') - host = host.decode('ascii') - - if host is None: - host = 'localhost' - if port is None: - port = 80 if scheme == 'http' else 443 - port = int(port) - - request = Request( - form_in, - method.encode('ascii'), - scheme.encode('ascii'), - host.encode('ascii'), - port, - path.encode('ascii'), - b"HTTP/2.0", - headers, - body, - timestamp_start, - timestamp_end, - ) - request.stream_id = stream_id - - return request - - def read_response( - self, - __rfile, - request_method=b'', - body_size_limit=None, - include_body=True, - stream_id=None, - ): - if body_size_limit is not None: - raise NotImplementedError() - - self.perform_connection_preface() - - timestamp_start = time.time() - if hasattr(self.tcp_handler.rfile, "reset_timestamps"): - self.tcp_handler.rfile.reset_timestamps() - - stream_id, headers, body = self._receive_transmission( - stream_id=stream_id, - include_body=include_body, - ) - - if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): - # more accurate timestamp_start - timestamp_start = self.tcp_handler.rfile.first_byte_timestamp - - if include_body: - timestamp_end = time.time() - else: - timestamp_end = None - - response = Response( - b"HTTP/2.0", - int(headers.get(':status', 502)), - b'', - headers, - body, - timestamp_start=timestamp_start, - timestamp_end=timestamp_end, - ) - response.stream_id = stream_id - - return response - - def assemble(self, message): - if isinstance(message, Request): - return self.assemble_request(message) - elif isinstance(message, Response): - return self.assemble_response(message) - else: - raise ValueError("HTTP message not supported.") - - def assemble_request(self, request): - assert isinstance(request, Request) - - authority = self.tcp_handler.sni if self.tcp_handler.sni else self.tcp_handler.address.host - if self.tcp_handler.address.port != 443: - authority += ":%d" % self.tcp_handler.address.port - - headers = request.headers.copy() - - if ':authority' not in headers: - headers.fields.insert(0, (b':authority', authority.encode('ascii'))) - if ':scheme' not in headers: - headers.fields.insert(0, (b':scheme', request.scheme.encode('ascii'))) - if ':path' not in headers: - headers.fields.insert(0, (b':path', request.path.encode('ascii'))) - if ':method' not in headers: - headers.fields.insert(0, (b':method', request.method.encode('ascii'))) - - if hasattr(request, 'stream_id'): - stream_id = request.stream_id - else: - stream_id = self._next_stream_id() - - return list(itertools.chain( - self._create_headers(headers, stream_id, end_stream=(request.body is None or len(request.body) == 0)), - self._create_body(request.body, stream_id))) - - def assemble_response(self, response): - assert isinstance(response, Response) - - headers = response.headers.copy() - - if ':status' not in headers: - headers.fields.insert(0, (b':status', str(response.status_code).encode('ascii'))) - - if hasattr(response, 'stream_id'): - stream_id = response.stream_id - else: - stream_id = self._next_stream_id() - - return list(itertools.chain( - self._create_headers(headers, stream_id, end_stream=(response.body is None or len(response.body) == 0)), - self._create_body(response.body, stream_id), - )) - - def perform_connection_preface(self, force=False): - if force or not self.connection_preface_performed: - if self.is_server: - self.perform_server_connection_preface(force) - else: - self.perform_client_connection_preface(force) - - def perform_server_connection_preface(self, force=False): - if force or not self.connection_preface_performed: - self.connection_preface_performed = True - - magic_length = len(self.CLIENT_CONNECTION_PREFACE) - magic = self.tcp_handler.rfile.safe_read(magic_length) - assert magic == self.CLIENT_CONNECTION_PREFACE - - frm = frame.SettingsFrame(settings={ - frame.SettingsFrame.ENABLE_PUSH: 0, - frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 1, - }) - self.send_frame(frm, hide=True) - self._receive_settings(hide=True) - - def perform_client_connection_preface(self, force=False): - if force or not self.connection_preface_performed: - self.connection_preface_performed = True - - self.tcp_handler.wfile.write(self.CLIENT_CONNECTION_PREFACE) - - self.send_frame(frame.SettingsFrame(), hide=True) - self._receive_settings(hide=True) # server announces own settings - self._receive_settings(hide=True) # server acks my settings - - def send_frame(self, frm, hide=False): - raw_bytes = frm.serialize() - self.tcp_handler.wfile.write(raw_bytes) - self.tcp_handler.wfile.flush() - if not hide and self.dump_frames: # pragma no cover - print(frm.human_readable(">>")) - - def read_frame(self, hide=False): - while True: - frm = utils.http2_read_frame(self.tcp_handler.rfile) - if not hide and self.dump_frames: # pragma no cover - print(frm.human_readable("<<")) - - if isinstance(frm, frame.PingFrame): - raw_bytes = frame.PingFrame(flags=['ACK'], payload=frm.payload).serialize() - self.tcp_handler.wfile.write(raw_bytes) - self.tcp_handler.wfile.flush() - continue - if isinstance(frm, frame.SettingsFrame) and 'ACK' not in frm.flags: - self._apply_settings(frm.settings, hide) - if isinstance(frm, frame.DataFrame) and frm.flow_controlled_length > 0: - self._update_flow_control_window(frm.stream_id, frm.flow_controlled_length) - return frm - - def check_alpn(self): - alp = self.tcp_handler.get_alpn_proto_negotiated() - if alp != b'h2': - raise NotImplementedError( - "HTTP2Protocol can not handle unknown ALP: %s" % alp) - return True - - def _handle_unexpected_frame(self, frm): - if isinstance(frm, frame.SettingsFrame): - return - if self.unhandled_frame_cb: - self.unhandled_frame_cb(frm) - - def _receive_settings(self, hide=False): - while True: - frm = self.read_frame(hide) - if isinstance(frm, frame.SettingsFrame): - break - else: - self._handle_unexpected_frame(frm) - - def _next_stream_id(self): - if self.current_stream_id is None: - if self.is_server: - # servers must use even stream ids - self.current_stream_id = 2 - else: - # clients must use odd stream ids - self.current_stream_id = 1 - else: - self.current_stream_id += 2 - return self.current_stream_id - - def _apply_settings(self, settings, hide=False): - for setting, value in settings.items(): - old_value = self.http2_settings[setting] - if not old_value: - old_value = '-' - self.http2_settings[setting] = value - - frm = frame.SettingsFrame(flags=['ACK']) - self.send_frame(frm, hide) - - def _update_flow_control_window(self, stream_id, increment): - frm = frame.WindowUpdateFrame(stream_id=0, window_increment=increment) - self.send_frame(frm) - frm = frame.WindowUpdateFrame(stream_id=stream_id, window_increment=increment) - self.send_frame(frm) - - def _create_headers(self, headers, stream_id, end_stream=True): - def frame_cls(chunks): - for i in chunks: - if i == 0: - yield frame.HeadersFrame, i - else: - yield frame.ContinuationFrame, i - - header_block_fragment = self.encoder.encode(headers.fields) - - chunk_size = self.http2_settings[frame.SettingsFrame.MAX_FRAME_SIZE] - chunks = range(0, len(header_block_fragment), chunk_size) - frms = [frm_cls( - flags=[], - stream_id=stream_id, - data=header_block_fragment[i:i+chunk_size]) for frm_cls, i in frame_cls(chunks)] - - frms[-1].flags.add('END_HEADERS') - if end_stream: - frms[0].flags.add('END_STREAM') - - if self.dump_frames: # pragma no cover - for frm in frms: - print(frm.human_readable(">>")) - - return [frm.serialize() for frm in frms] - - def _create_body(self, body, stream_id): - if body is None or len(body) == 0: - return b'' - - chunk_size = self.http2_settings[frame.SettingsFrame.MAX_FRAME_SIZE] - chunks = range(0, len(body), chunk_size) - frms = [frame.DataFrame( - flags=[], - stream_id=stream_id, - data=body[i:i+chunk_size]) for i in chunks] - frms[-1].flags.add('END_STREAM') - - if self.dump_frames: # pragma no cover - for frm in frms: - print(frm.human_readable(">>")) - - return [frm.serialize() for frm in frms] - - def _receive_transmission(self, stream_id=None, include_body=True): - if not include_body: - raise NotImplementedError() - - body_expected = True - - header_blocks = b'' - body = b'' - - while True: - frm = self.read_frame() - if ( - (isinstance(frm, frame.HeadersFrame) or isinstance(frm, frame.ContinuationFrame)) and - (stream_id is None or frm.stream_id == stream_id) - ): - stream_id = frm.stream_id - header_blocks += frm.data - if 'END_STREAM' in frm.flags: - body_expected = False - if 'END_HEADERS' in frm.flags: - break - else: - self._handle_unexpected_frame(frm) - - while body_expected: - frm = self.read_frame() - if isinstance(frm, frame.DataFrame) and frm.stream_id == stream_id: - body += frm.data - if 'END_STREAM' in frm.flags: - break - else: - self._handle_unexpected_frame(frm) - - headers = Headers( - [[k.encode('ascii'), v.encode('ascii')] for k, v in self.decoder.decode(header_blocks)] - ) - - return stream_id, headers, body diff --git a/netlib/netlib/http/message.py b/netlib/netlib/http/message.py deleted file mode 100644 index e3d8ce37..00000000 --- a/netlib/netlib/http/message.py +++ /dev/null @@ -1,222 +0,0 @@ -from __future__ import absolute_import, print_function, division - -import warnings - -import six - -from .headers import Headers -from .. import encoding, utils - -CONTENT_MISSING = 0 - -if six.PY2: # pragma: nocover - _native = lambda x: x - _always_bytes = lambda x: x -else: - # While the HTTP head _should_ be ASCII, it's not uncommon for certain headers to be utf-8 encoded. - _native = lambda x: x.decode("utf-8", "surrogateescape") - _always_bytes = lambda x: utils.always_bytes(x, "utf-8", "surrogateescape") - - -class MessageData(utils.Serializable): - def __eq__(self, other): - if isinstance(other, MessageData): - return self.__dict__ == other.__dict__ - return False - - def __ne__(self, other): - return not self.__eq__(other) - - def set_state(self, state): - for k, v in state.items(): - if k == "headers": - v = Headers.from_state(v) - setattr(self, k, v) - - def get_state(self): - state = vars(self).copy() - state["headers"] = state["headers"].get_state() - return state - - @classmethod - def from_state(cls, state): - state["headers"] = Headers.from_state(state["headers"]) - return cls(**state) - - -class Message(utils.Serializable): - def __init__(self, data): - self.data = data - - def __eq__(self, other): - if isinstance(other, Message): - return self.data == other.data - return False - - def __ne__(self, other): - return not self.__eq__(other) - - def get_state(self): - return self.data.get_state() - - def set_state(self, state): - self.data.set_state(state) - - @classmethod - def from_state(cls, state): - return cls(**state) - - @property - def headers(self): - """ - Message headers object - - Returns: - netlib.http.Headers - """ - return self.data.headers - - @headers.setter - def headers(self, h): - self.data.headers = h - - @property - def content(self): - """ - The raw (encoded) HTTP message body - - See also: :py:attr:`text` - """ - return self.data.content - - @content.setter - def content(self, content): - self.data.content = content - if isinstance(content, bytes): - self.headers["content-length"] = str(len(content)) - - @property - def http_version(self): - """ - Version string, e.g. "HTTP/1.1" - """ - return _native(self.data.http_version) - - @http_version.setter - def http_version(self, http_version): - self.data.http_version = _always_bytes(http_version) - - @property - def timestamp_start(self): - """ - First byte timestamp - """ - return self.data.timestamp_start - - @timestamp_start.setter - def timestamp_start(self, timestamp_start): - self.data.timestamp_start = timestamp_start - - @property - def timestamp_end(self): - """ - Last byte timestamp - """ - return self.data.timestamp_end - - @timestamp_end.setter - def timestamp_end(self, timestamp_end): - self.data.timestamp_end = timestamp_end - - @property - def text(self): - """ - The decoded HTTP message body. - Decoded contents are not cached, so accessing this attribute repeatedly is relatively expensive. - - .. note:: - This is not implemented yet. - - See also: :py:attr:`content`, :py:class:`decoded` - """ - # This attribute should be called text, because that's what requests does. - raise NotImplementedError() - - @text.setter - def text(self, text): - raise NotImplementedError() - - def decode(self): - """ - Decodes body based on the current Content-Encoding header, then - removes the header. If there is no Content-Encoding header, no - action is taken. - - Returns: - True, if decoding succeeded. - False, otherwise. - """ - ce = self.headers.get("content-encoding") - data = encoding.decode(ce, self.content) - if data is None: - return False - self.content = data - self.headers.pop("content-encoding", None) - return True - - def encode(self, e): - """ - Encodes body with the encoding e, where e is "gzip", "deflate" or "identity". - - Returns: - True, if decoding succeeded. - False, otherwise. - """ - data = encoding.encode(e, self.content) - if data is None: - return False - self.content = data - self.headers["content-encoding"] = e - return True - - # Legacy - - @property - def body(self): # pragma: nocover - warnings.warn(".body is deprecated, use .content instead.", DeprecationWarning) - return self.content - - @body.setter - def body(self, body): # pragma: nocover - warnings.warn(".body is deprecated, use .content instead.", DeprecationWarning) - self.content = body - - -class decoded(object): - """ - A context manager that decodes a request or response, and then - re-encodes it with the same encoding after execution of the block. - - Example: - - .. code-block:: python - - with decoded(request): - request.content = request.content.replace("foo", "bar") - """ - - def __init__(self, message): - self.message = message - ce = message.headers.get("content-encoding") - if ce in encoding.ENCODINGS: - self.ce = ce - else: - self.ce = None - - def __enter__(self): - if self.ce: - self.message.decode() - - def __exit__(self, type, value, tb): - if self.ce: - self.message.encode(self.ce) diff --git a/netlib/netlib/http/request.py b/netlib/netlib/http/request.py deleted file mode 100644 index b9076c0f..00000000 --- a/netlib/netlib/http/request.py +++ /dev/null @@ -1,356 +0,0 @@ -from __future__ import absolute_import, print_function, division - -import warnings - -import six -from six.moves import urllib - -from netlib import utils -from netlib.http import cookies -from netlib.odict import ODict -from .. import encoding -from .headers import Headers -from .message import Message, _native, _always_bytes, MessageData - - -class RequestData(MessageData): - def __init__(self, first_line_format, method, scheme, host, port, path, http_version, headers=None, content=None, - timestamp_start=None, timestamp_end=None): - if not isinstance(headers, Headers): - headers = Headers(headers) - - self.first_line_format = first_line_format - self.method = method - self.scheme = scheme - self.host = host - self.port = port - self.path = path - self.http_version = http_version - self.headers = headers - self.content = content - self.timestamp_start = timestamp_start - self.timestamp_end = timestamp_end - - -class Request(Message): - """ - An HTTP request. - """ - def __init__(self, *args, **kwargs): - data = RequestData(*args, **kwargs) - super(Request, self).__init__(data) - - def __repr__(self): - if self.host and self.port: - hostport = "{}:{}".format(self.host, self.port) - else: - hostport = "" - path = self.path or "" - return "Request({} {}{})".format( - self.method, hostport, path - ) - - @property - def first_line_format(self): - """ - HTTP request form as defined in `RFC7230 `_. - - origin-form and asterisk-form are subsumed as "relative". - """ - return self.data.first_line_format - - @first_line_format.setter - def first_line_format(self, first_line_format): - self.data.first_line_format = first_line_format - - @property - def method(self): - """ - HTTP request method, e.g. "GET". - """ - return _native(self.data.method).upper() - - @method.setter - def method(self, method): - self.data.method = _always_bytes(method) - - @property - def scheme(self): - """ - HTTP request scheme, which should be "http" or "https". - """ - return _native(self.data.scheme) - - @scheme.setter - def scheme(self, scheme): - self.data.scheme = _always_bytes(scheme) - - @property - def host(self): - """ - Target host. This may be parsed from the raw request - (e.g. from a ``GET http://example.com/ HTTP/1.1`` request line) - or inferred from the proxy mode (e.g. an IP in transparent mode). - - Setting the host attribute also updates the host header, if present. - """ - - if six.PY2: # pragma: nocover - return self.data.host - - if not self.data.host: - return self.data.host - try: - return self.data.host.decode("idna") - except UnicodeError: - return self.data.host.decode("utf8", "surrogateescape") - - @host.setter - def host(self, host): - if isinstance(host, six.text_type): - try: - # There's no non-strict mode for IDNA encoding. - # We don't want this operation to fail though, so we try - # utf8 as a last resort. - host = host.encode("idna", "strict") - except UnicodeError: - host = host.encode("utf8", "surrogateescape") - - self.data.host = host - - # Update host header - if "host" in self.headers: - if host: - self.headers["host"] = host - else: - self.headers.pop("host") - - @property - def port(self): - """ - Target port - """ - return self.data.port - - @port.setter - def port(self, port): - self.data.port = port - - @property - def path(self): - """ - HTTP request path, e.g. "/index.html". - Guaranteed to start with a slash. - """ - return _native(self.data.path) - - @path.setter - def path(self, path): - self.data.path = _always_bytes(path) - - @property - def url(self): - """ - The URL string, constructed from the request's URL components - """ - return utils.unparse_url(self.scheme, self.host, self.port, self.path) - - @url.setter - def url(self, url): - self.scheme, self.host, self.port, self.path = utils.parse_url(url) - - @property - def pretty_host(self): - """ - Similar to :py:attr:`host`, but using the Host headers as an additional preferred data source. - This is useful in transparent mode where :py:attr:`host` is only an IP address, - but may not reflect the actual destination as the Host header could be spoofed. - """ - return self.headers.get("host", self.host) - - @property - def pretty_url(self): - """ - Like :py:attr:`url`, but using :py:attr:`pretty_host` instead of :py:attr:`host`. - """ - if self.first_line_format == "authority": - return "%s:%d" % (self.pretty_host, self.port) - return utils.unparse_url(self.scheme, self.pretty_host, self.port, self.path) - - @property - def query(self): - """ - The request query string as an :py:class:`ODict` object. - None, if there is no query. - """ - _, _, _, _, query, _ = urllib.parse.urlparse(self.url) - if query: - return ODict(utils.urldecode(query)) - return None - - @query.setter - def query(self, odict): - query = utils.urlencode(odict.lst) - scheme, netloc, path, params, _, fragment = urllib.parse.urlparse(self.url) - _, _, _, self.path = utils.parse_url( - urllib.parse.urlunparse([scheme, netloc, path, params, query, fragment])) - - @property - def cookies(self): - """ - The request cookies. - An empty :py:class:`ODict` object if the cookie monster ate them all. - """ - ret = ODict() - for i in self.headers.get_all("Cookie"): - ret.extend(cookies.parse_cookie_header(i)) - return ret - - @cookies.setter - def cookies(self, odict): - self.headers["cookie"] = cookies.format_cookie_header(odict) - - @property - def path_components(self): - """ - The URL's path components as a list of strings. - Components are unquoted. - """ - _, _, path, _, _, _ = urllib.parse.urlparse(self.url) - return [urllib.parse.unquote(i) for i in path.split("/") if i] - - @path_components.setter - def path_components(self, components): - components = map(lambda x: urllib.parse.quote(x, safe=""), components) - path = "/" + "/".join(components) - scheme, netloc, _, params, query, fragment = urllib.parse.urlparse(self.url) - _, _, _, self.path = utils.parse_url( - urllib.parse.urlunparse([scheme, netloc, path, params, query, fragment])) - - def anticache(self): - """ - Modifies this request to remove headers that might produce a cached - response. That is, we remove ETags and If-Modified-Since headers. - """ - delheaders = [ - "if-modified-since", - "if-none-match", - ] - for i in delheaders: - self.headers.pop(i, None) - - def anticomp(self): - """ - Modifies this request to remove headers that will compress the - resource's data. - """ - self.headers["accept-encoding"] = "identity" - - def constrain_encoding(self): - """ - Limits the permissible Accept-Encoding values, based on what we can - decode appropriately. - """ - accept_encoding = self.headers.get("accept-encoding") - if accept_encoding: - self.headers["accept-encoding"] = ( - ', '.join( - e - for e in encoding.ENCODINGS - if e in accept_encoding - ) - ) - - @property - def urlencoded_form(self): - """ - The URL-encoded form data as an :py:class:`ODict` object. - None if there is no data or the content-type indicates non-form data. - """ - is_valid_content_type = "application/x-www-form-urlencoded" in self.headers.get("content-type", "").lower() - if self.content and is_valid_content_type: - return ODict(utils.urldecode(self.content)) - return None - - @urlencoded_form.setter - def urlencoded_form(self, odict): - """ - Sets the body to the URL-encoded form data, and adds the appropriate content-type header. - This will overwrite the existing content if there is one. - """ - self.headers["content-type"] = "application/x-www-form-urlencoded" - self.content = utils.urlencode(odict.lst) - - @property - def multipart_form(self): - """ - The multipart form data as an :py:class:`ODict` object. - None if there is no data or the content-type indicates non-form data. - """ - is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower() - if self.content and is_valid_content_type: - return ODict(utils.multipartdecode(self.headers,self.content)) - return None - - @multipart_form.setter - def multipart_form(self, value): - raise NotImplementedError() - - # Legacy - - def get_cookies(self): # pragma: nocover - warnings.warn(".get_cookies is deprecated, use .cookies instead.", DeprecationWarning) - return self.cookies - - def set_cookies(self, odict): # pragma: nocover - warnings.warn(".set_cookies is deprecated, use .cookies instead.", DeprecationWarning) - self.cookies = odict - - def get_query(self): # pragma: nocover - warnings.warn(".get_query is deprecated, use .query instead.", DeprecationWarning) - return self.query or ODict([]) - - def set_query(self, odict): # pragma: nocover - warnings.warn(".set_query is deprecated, use .query instead.", DeprecationWarning) - self.query = odict - - def get_path_components(self): # pragma: nocover - warnings.warn(".get_path_components is deprecated, use .path_components instead.", DeprecationWarning) - return self.path_components - - def set_path_components(self, lst): # pragma: nocover - warnings.warn(".set_path_components is deprecated, use .path_components instead.", DeprecationWarning) - self.path_components = lst - - def get_form_urlencoded(self): # pragma: nocover - warnings.warn(".get_form_urlencoded is deprecated, use .urlencoded_form instead.", DeprecationWarning) - return self.urlencoded_form or ODict([]) - - def set_form_urlencoded(self, odict): # pragma: nocover - warnings.warn(".set_form_urlencoded is deprecated, use .urlencoded_form instead.", DeprecationWarning) - self.urlencoded_form = odict - - def get_form_multipart(self): # pragma: nocover - warnings.warn(".get_form_multipart is deprecated, use .multipart_form instead.", DeprecationWarning) - return self.multipart_form or ODict([]) - - @property - def form_in(self): # pragma: nocover - warnings.warn(".form_in is deprecated, use .first_line_format instead.", DeprecationWarning) - return self.first_line_format - - @form_in.setter - def form_in(self, form_in): # pragma: nocover - warnings.warn(".form_in is deprecated, use .first_line_format instead.", DeprecationWarning) - self.first_line_format = form_in - - @property - def form_out(self): # pragma: nocover - warnings.warn(".form_out is deprecated, use .first_line_format instead.", DeprecationWarning) - return self.first_line_format - - @form_out.setter - def form_out(self, form_out): # pragma: nocover - warnings.warn(".form_out is deprecated, use .first_line_format instead.", DeprecationWarning) - self.first_line_format = form_out - diff --git a/netlib/netlib/http/response.py b/netlib/netlib/http/response.py deleted file mode 100644 index 8f4d6215..00000000 --- a/netlib/netlib/http/response.py +++ /dev/null @@ -1,116 +0,0 @@ -from __future__ import absolute_import, print_function, division - -import warnings - -from . import cookies -from .headers import Headers -from .message import Message, _native, _always_bytes, MessageData -from .. import utils -from ..odict import ODict - - -class ResponseData(MessageData): - def __init__(self, http_version, status_code, reason=None, headers=None, content=None, - timestamp_start=None, timestamp_end=None): - if not isinstance(headers, Headers): - headers = Headers(headers) - - self.http_version = http_version - self.status_code = status_code - self.reason = reason - self.headers = headers - self.content = content - self.timestamp_start = timestamp_start - self.timestamp_end = timestamp_end - - -class Response(Message): - """ - An HTTP response. - """ - def __init__(self, *args, **kwargs): - data = ResponseData(*args, **kwargs) - super(Response, self).__init__(data) - - def __repr__(self): - if self.content: - details = "{}, {}".format( - self.headers.get("content-type", "unknown content type"), - utils.pretty_size(len(self.content)) - ) - else: - details = "no content" - return "Response({status_code} {reason}, {details})".format( - status_code=self.status_code, - reason=self.reason, - details=details - ) - - @property - def status_code(self): - """ - HTTP Status Code, e.g. ``200``. - """ - return self.data.status_code - - @status_code.setter - def status_code(self, status_code): - self.data.status_code = status_code - - @property - def reason(self): - """ - HTTP Reason Phrase, e.g. "Not Found". - This is always :py:obj:`None` for HTTP2 requests, because HTTP2 responses do not contain a reason phrase. - """ - return _native(self.data.reason) - - @reason.setter - def reason(self, reason): - self.data.reason = _always_bytes(reason) - - @property - def cookies(self): - """ - Get the contents of all Set-Cookie headers. - - A possibly empty :py:class:`ODict`, where keys are cookie name strings, - and values are [value, attr] lists. Value is a string, and attr is - an ODictCaseless containing cookie attributes. Within attrs, unary - attributes (e.g. HTTPOnly) are indicated by a Null value. - """ - ret = [] - for header in self.headers.get_all("set-cookie"): - v = cookies.parse_set_cookie_header(header) - if v: - name, value, attrs = v - ret.append([name, [value, attrs]]) - return ODict(ret) - - @cookies.setter - def cookies(self, odict): - values = [] - for i in odict.lst: - header = cookies.format_set_cookie_header(i[0], i[1][0], i[1][1]) - values.append(header) - self.headers.set_all("set-cookie", values) - - # Legacy - - def get_cookies(self): # pragma: nocover - warnings.warn(".get_cookies is deprecated, use .cookies instead.", DeprecationWarning) - return self.cookies - - def set_cookies(self, odict): # pragma: nocover - warnings.warn(".set_cookies is deprecated, use .cookies instead.", DeprecationWarning) - self.cookies = odict - - @property - def msg(self): # pragma: nocover - warnings.warn(".msg is deprecated, use .reason instead.", DeprecationWarning) - return self.reason - - @msg.setter - def msg(self, reason): # pragma: nocover - warnings.warn(".msg is deprecated, use .reason instead.", DeprecationWarning) - self.reason = reason diff --git a/netlib/netlib/http/status_codes.py b/netlib/netlib/http/status_codes.py deleted file mode 100644 index 8a4dc1f5..00000000 --- a/netlib/netlib/http/status_codes.py +++ /dev/null @@ -1,106 +0,0 @@ -from __future__ import absolute_import, print_function, division - -CONTINUE = 100 -SWITCHING = 101 -OK = 200 -CREATED = 201 -ACCEPTED = 202 -NON_AUTHORITATIVE_INFORMATION = 203 -NO_CONTENT = 204 -RESET_CONTENT = 205 -PARTIAL_CONTENT = 206 -MULTI_STATUS = 207 - -MULTIPLE_CHOICE = 300 -MOVED_PERMANENTLY = 301 -FOUND = 302 -SEE_OTHER = 303 -NOT_MODIFIED = 304 -USE_PROXY = 305 -TEMPORARY_REDIRECT = 307 - -BAD_REQUEST = 400 -UNAUTHORIZED = 401 -PAYMENT_REQUIRED = 402 -FORBIDDEN = 403 -NOT_FOUND = 404 -NOT_ALLOWED = 405 -NOT_ACCEPTABLE = 406 -PROXY_AUTH_REQUIRED = 407 -REQUEST_TIMEOUT = 408 -CONFLICT = 409 -GONE = 410 -LENGTH_REQUIRED = 411 -PRECONDITION_FAILED = 412 -REQUEST_ENTITY_TOO_LARGE = 413 -REQUEST_URI_TOO_LONG = 414 -UNSUPPORTED_MEDIA_TYPE = 415 -REQUESTED_RANGE_NOT_SATISFIABLE = 416 -EXPECTATION_FAILED = 417 -IM_A_TEAPOT = 418 - -INTERNAL_SERVER_ERROR = 500 -NOT_IMPLEMENTED = 501 -BAD_GATEWAY = 502 -SERVICE_UNAVAILABLE = 503 -GATEWAY_TIMEOUT = 504 -HTTP_VERSION_NOT_SUPPORTED = 505 -INSUFFICIENT_STORAGE_SPACE = 507 -NOT_EXTENDED = 510 - -RESPONSES = { - # 100 - CONTINUE: "Continue", - SWITCHING: "Switching Protocols", - - # 200 - OK: "OK", - CREATED: "Created", - ACCEPTED: "Accepted", - NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information", - NO_CONTENT: "No Content", - RESET_CONTENT: "Reset Content.", - PARTIAL_CONTENT: "Partial Content", - MULTI_STATUS: "Multi-Status", - - # 300 - MULTIPLE_CHOICE: "Multiple Choices", - MOVED_PERMANENTLY: "Moved Permanently", - FOUND: "Found", - SEE_OTHER: "See Other", - NOT_MODIFIED: "Not Modified", - USE_PROXY: "Use Proxy", - # 306 not defined?? - TEMPORARY_REDIRECT: "Temporary Redirect", - - # 400 - BAD_REQUEST: "Bad Request", - UNAUTHORIZED: "Unauthorized", - PAYMENT_REQUIRED: "Payment Required", - FORBIDDEN: "Forbidden", - NOT_FOUND: "Not Found", - NOT_ALLOWED: "Method Not Allowed", - NOT_ACCEPTABLE: "Not Acceptable", - PROXY_AUTH_REQUIRED: "Proxy Authentication Required", - REQUEST_TIMEOUT: "Request Time-out", - CONFLICT: "Conflict", - GONE: "Gone", - LENGTH_REQUIRED: "Length Required", - PRECONDITION_FAILED: "Precondition Failed", - REQUEST_ENTITY_TOO_LARGE: "Request Entity Too Large", - REQUEST_URI_TOO_LONG: "Request-URI Too Long", - UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type", - REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable", - EXPECTATION_FAILED: "Expectation Failed", - IM_A_TEAPOT: "I'm a teapot", - - # 500 - INTERNAL_SERVER_ERROR: "Internal Server Error", - NOT_IMPLEMENTED: "Not Implemented", - BAD_GATEWAY: "Bad Gateway", - SERVICE_UNAVAILABLE: "Service Unavailable", - GATEWAY_TIMEOUT: "Gateway Time-out", - HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported", - INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space", - NOT_EXTENDED: "Not Extended" -} diff --git a/netlib/netlib/http/user_agents.py b/netlib/netlib/http/user_agents.py deleted file mode 100644 index e8681908..00000000 --- a/netlib/netlib/http/user_agents.py +++ /dev/null @@ -1,52 +0,0 @@ -from __future__ import (absolute_import, print_function, division) - -""" - A small collection of useful user-agent header strings. These should be - kept reasonably current to reflect common usage. -""" - -# pylint: line-too-long - -# A collection of (name, shortcut, string) tuples. - -UASTRINGS = [ - ("android", - "a", - "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Nexus 7 Build/JRO03D) AFL/01.04.02"), # noqa - ("blackberry", - "l", - "Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+"), # noqa - ("bingbot", - "b", - "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"), # noqa - ("chrome", - "c", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"), # noqa - ("firefox", - "f", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1"), # noqa - ("googlebot", - "g", - "Googlebot/2.1 (+http://www.googlebot.com/bot.html)"), # noqa - ("ie9", - "i", - "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US)"), # noqa - ("ipad", - "p", - "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3"), # noqa - ("iphone", - "h", - "Mozilla/5.0 (iPhone; CPU iPhone OS 4_2_1 like Mac OS X) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5"), # noqa - ("safari", - "s", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10"), # noqa -] - - -def get_by_shortcut(s): - """ - Retrieve a user agent entry by shortcut. - """ - for i in UASTRINGS: - if s == i[1]: - return i diff --git a/netlib/netlib/odict.py b/netlib/netlib/odict.py deleted file mode 100644 index 1e6e381a..00000000 --- a/netlib/netlib/odict.py +++ /dev/null @@ -1,193 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -import re -import copy -import six - -from .utils import Serializable - - -def safe_subn(pattern, repl, target, *args, **kwargs): - """ - There are Unicode conversion problems with re.subn. We try to smooth - that over by casting the pattern and replacement to strings. We really - need a better solution that is aware of the actual content ecoding. - """ - return re.subn(str(pattern), str(repl), target, *args, **kwargs) - - -class ODict(Serializable): - - """ - A dictionary-like object for managing ordered (key, value) data. Think - about it as a convenient interface to a list of (key, value) tuples. - """ - - def __init__(self, lst=None): - self.lst = lst or [] - - def _kconv(self, s): - return s - - def __eq__(self, other): - return self.lst == other.lst - - def __ne__(self, other): - return not self.__eq__(other) - - def __iter__(self): - return self.lst.__iter__() - - def __getitem__(self, k): - """ - Returns a list of values matching key. - """ - ret = [] - k = self._kconv(k) - for i in self.lst: - if self._kconv(i[0]) == k: - ret.append(i[1]) - return ret - - def keys(self): - return list(set([self._kconv(i[0]) for i in self.lst])) - - def _filter_lst(self, k, lst): - k = self._kconv(k) - new = [] - for i in lst: - if self._kconv(i[0]) != k: - new.append(i) - return new - - def __len__(self): - """ - Total number of (key, value) pairs. - """ - return len(self.lst) - - def __setitem__(self, k, valuelist): - """ - Sets the values for key k. If there are existing values for this - key, they are cleared. - """ - if isinstance(valuelist, six.text_type) or isinstance(valuelist, six.binary_type): - raise ValueError( - "Expected list of values instead of string. " - "Example: odict[b'Host'] = [b'www.example.com']" - ) - kc = self._kconv(k) - new = [] - for i in self.lst: - if self._kconv(i[0]) == kc: - if valuelist: - new.append([k, valuelist.pop(0)]) - else: - new.append(i) - while valuelist: - new.append([k, valuelist.pop(0)]) - self.lst = new - - def __delitem__(self, k): - """ - Delete all items matching k. - """ - self.lst = self._filter_lst(k, self.lst) - - def __contains__(self, k): - k = self._kconv(k) - for i in self.lst: - if self._kconv(i[0]) == k: - return True - return False - - def add(self, key, value, prepend=False): - if prepend: - self.lst.insert(0, [key, value]) - else: - self.lst.append([key, value]) - - def get(self, k, d=None): - if k in self: - return self[k] - else: - return d - - def get_first(self, k, d=None): - if k in self: - return self[k][0] - else: - return d - - def items(self): - return self.lst[:] - - def copy(self): - """ - Returns a copy of this object. - """ - lst = copy.deepcopy(self.lst) - return self.__class__(lst) - - def extend(self, other): - """ - Add the contents of other, preserving any duplicates. - """ - self.lst.extend(other.lst) - - def __repr__(self): - return repr(self.lst) - - def in_any(self, key, value, caseless=False): - """ - Do any of the values matching key contain value? - - If caseless is true, value comparison is case-insensitive. - """ - if caseless: - value = value.lower() - for i in self[key]: - if caseless: - i = i.lower() - if value in i: - return True - return False - - def replace(self, pattern, repl, *args, **kwargs): - """ - Replaces a regular expression pattern with repl in both keys and - values. Encoded content will be decoded before replacement, and - re-encoded afterwards. - - Returns the number of replacements made. - """ - nlst, count = [], 0 - for i in self.lst: - k, c = safe_subn(pattern, repl, i[0], *args, **kwargs) - count += c - v, c = safe_subn(pattern, repl, i[1], *args, **kwargs) - count += c - nlst.append([k, v]) - self.lst = nlst - return count - - # Implement the StateObject protocol from mitmproxy - def get_state(self): - return [tuple(i) for i in self.lst] - - def set_state(self, state): - self.lst = [list(i) for i in state] - - @classmethod - def from_state(cls, state): - return cls([list(i) for i in state]) - - -class ODictCaseless(ODict): - - """ - A variant of ODict with "caseless" keys. This version _preserves_ key - case, but does not consider case when setting or getting items. - """ - - def _kconv(self, s): - return s.lower() diff --git a/netlib/netlib/socks.py b/netlib/netlib/socks.py deleted file mode 100644 index 51ad1c63..00000000 --- a/netlib/netlib/socks.py +++ /dev/null @@ -1,176 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -import struct -import array -import ipaddress -from . import tcp, utils - - -class SocksError(Exception): - def __init__(self, code, message): - super(SocksError, self).__init__(message) - self.code = code - - -VERSION = utils.BiDi( - SOCKS4=0x04, - SOCKS5=0x05 -) - -CMD = utils.BiDi( - CONNECT=0x01, - BIND=0x02, - UDP_ASSOCIATE=0x03 -) - -ATYP = utils.BiDi( - IPV4_ADDRESS=0x01, - DOMAINNAME=0x03, - IPV6_ADDRESS=0x04 -) - -REP = utils.BiDi( - SUCCEEDED=0x00, - GENERAL_SOCKS_SERVER_FAILURE=0x01, - CONNECTION_NOT_ALLOWED_BY_RULESET=0x02, - NETWORK_UNREACHABLE=0x03, - HOST_UNREACHABLE=0x04, - CONNECTION_REFUSED=0x05, - TTL_EXPIRED=0x06, - COMMAND_NOT_SUPPORTED=0x07, - ADDRESS_TYPE_NOT_SUPPORTED=0x08, -) - -METHOD = utils.BiDi( - NO_AUTHENTICATION_REQUIRED=0x00, - GSSAPI=0x01, - USERNAME_PASSWORD=0x02, - NO_ACCEPTABLE_METHODS=0xFF -) - - -class ClientGreeting(object): - __slots__ = ("ver", "methods") - - def __init__(self, ver, methods): - self.ver = ver - self.methods = array.array("B") - self.methods.extend(methods) - - def assert_socks5(self): - if self.ver != VERSION.SOCKS5: - if self.ver == ord("G") and len(self.methods) == ord("E"): - guess = "Probably not a SOCKS request but a regular HTTP request. " - else: - guess = "" - - raise SocksError( - REP.GENERAL_SOCKS_SERVER_FAILURE, - guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.ver - ) - - @classmethod - def from_file(cls, f, fail_early=False): - """ - :param fail_early: If true, a SocksError will be raised if the first byte does not indicate socks5. - """ - ver, nmethods = struct.unpack("!BB", f.safe_read(2)) - client_greeting = cls(ver, []) - if fail_early: - client_greeting.assert_socks5() - client_greeting.methods.fromstring(f.safe_read(nmethods)) - return client_greeting - - def to_file(self, f): - f.write(struct.pack("!BB", self.ver, len(self.methods))) - f.write(self.methods.tostring()) - - -class ServerGreeting(object): - __slots__ = ("ver", "method") - - def __init__(self, ver, method): - self.ver = ver - self.method = method - - def assert_socks5(self): - if self.ver != VERSION.SOCKS5: - if self.ver == ord("H") and self.method == ord("T"): - guess = "Probably not a SOCKS request but a regular HTTP response. " - else: - guess = "" - - raise SocksError( - REP.GENERAL_SOCKS_SERVER_FAILURE, - guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.ver - ) - - @classmethod - def from_file(cls, f): - ver, method = struct.unpack("!BB", f.safe_read(2)) - return cls(ver, method) - - def to_file(self, f): - f.write(struct.pack("!BB", self.ver, self.method)) - - -class Message(object): - __slots__ = ("ver", "msg", "atyp", "addr") - - def __init__(self, ver, msg, atyp, addr): - self.ver = ver - self.msg = msg - self.atyp = atyp - self.addr = tcp.Address.wrap(addr) - - def assert_socks5(self): - if self.ver != VERSION.SOCKS5: - raise SocksError( - REP.GENERAL_SOCKS_SERVER_FAILURE, - "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.ver - ) - - @classmethod - def from_file(cls, f): - ver, msg, rsv, atyp = struct.unpack("!BBBB", f.safe_read(4)) - if rsv != 0x00: - raise SocksError( - REP.GENERAL_SOCKS_SERVER_FAILURE, - "Socks Request: Invalid reserved byte: %s" % rsv - ) - if atyp == ATYP.IPV4_ADDRESS: - # We use tnoa here as ntop is not commonly available on Windows. - host = ipaddress.IPv4Address(f.safe_read(4)).compressed - use_ipv6 = False - elif atyp == ATYP.IPV6_ADDRESS: - host = ipaddress.IPv6Address(f.safe_read(16)).compressed - use_ipv6 = True - elif atyp == ATYP.DOMAINNAME: - length, = struct.unpack("!B", f.safe_read(1)) - host = f.safe_read(length) - if not utils.is_valid_host(host): - raise SocksError(REP.GENERAL_SOCKS_SERVER_FAILURE, "Invalid hostname: %s" % host) - host = host.decode("idna") - use_ipv6 = False - else: - raise SocksError(REP.ADDRESS_TYPE_NOT_SUPPORTED, - "Socks Request: Unknown ATYP: %s" % atyp) - - port, = struct.unpack("!H", f.safe_read(2)) - addr = tcp.Address((host, port), use_ipv6=use_ipv6) - return cls(ver, msg, atyp, addr) - - def to_file(self, f): - f.write(struct.pack("!BBBB", self.ver, self.msg, 0x00, self.atyp)) - if self.atyp == ATYP.IPV4_ADDRESS: - f.write(ipaddress.IPv4Address(self.addr.host).packed) - elif self.atyp == ATYP.IPV6_ADDRESS: - f.write(ipaddress.IPv6Address(self.addr.host).packed) - elif self.atyp == ATYP.DOMAINNAME: - f.write(struct.pack("!B", len(self.addr.host))) - f.write(self.addr.host.encode("idna")) - else: - raise SocksError( - REP.ADDRESS_TYPE_NOT_SUPPORTED, - "Unknown ATYP: %s" % self.atyp - ) - f.write(struct.pack("!H", self.addr.port)) diff --git a/netlib/netlib/tcp.py b/netlib/netlib/tcp.py deleted file mode 100644 index 61b41cdc..00000000 --- a/netlib/netlib/tcp.py +++ /dev/null @@ -1,911 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -import os -import select -import socket -import sys -import threading -import time -import traceback - -import binascii -from six.moves import range - -import certifi -from backports import ssl_match_hostname -import six -import OpenSSL -from OpenSSL import SSL - -from . import certutils, version_check, utils - -# This is a rather hackish way to make sure that -# the latest version of pyOpenSSL is actually installed. -from netlib.exceptions import InvalidCertificateException, TcpReadIncomplete, TlsException, \ - TcpTimeout, TcpDisconnect, TcpException - -version_check.check_pyopenssl_version() - -if six.PY2: - socket_fileobject = socket._fileobject -else: - socket_fileobject = socket.SocketIO - -EINTR = 4 -if os.environ.get("NO_ALPN"): - HAS_ALPN = False -else: - HAS_ALPN = OpenSSL._util.lib.Cryptography_HAS_ALPN - -# To enable all SSL methods use: SSLv23 -# then add options to disable certain methods -# https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 -SSL_BASIC_OPTIONS = ( - SSL.OP_CIPHER_SERVER_PREFERENCE -) -if hasattr(SSL, "OP_NO_COMPRESSION"): - SSL_BASIC_OPTIONS |= SSL.OP_NO_COMPRESSION - -SSL_DEFAULT_METHOD = SSL.SSLv23_METHOD -SSL_DEFAULT_OPTIONS = ( - SSL.OP_NO_SSLv2 | - SSL.OP_NO_SSLv3 | - SSL_BASIC_OPTIONS -) -if hasattr(SSL, "OP_NO_COMPRESSION"): - SSL_DEFAULT_OPTIONS |= SSL.OP_NO_COMPRESSION - -""" -Map a reasonable SSL version specification into the format OpenSSL expects. -Don't ask... -https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 -""" -sslversion_choices = { - "all": (SSL.SSLv23_METHOD, SSL_BASIC_OPTIONS), - # SSLv23_METHOD + NO_SSLv2 + NO_SSLv3 == TLS 1.0+ - # TLSv1_METHOD would be TLS 1.0 only - "secure": (SSL.SSLv23_METHOD, (SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 | SSL_BASIC_OPTIONS)), - "SSLv2": (SSL.SSLv2_METHOD, SSL_BASIC_OPTIONS), - "SSLv3": (SSL.SSLv3_METHOD, SSL_BASIC_OPTIONS), - "TLSv1": (SSL.TLSv1_METHOD, SSL_BASIC_OPTIONS), - "TLSv1_1": (SSL.TLSv1_1_METHOD, SSL_BASIC_OPTIONS), - "TLSv1_2": (SSL.TLSv1_2_METHOD, SSL_BASIC_OPTIONS), -} - -class SSLKeyLogger(object): - - def __init__(self, filename): - self.filename = filename - self.f = None - self.lock = threading.Lock() - - # required for functools.wraps, which pyOpenSSL uses. - __name__ = "SSLKeyLogger" - - def __call__(self, connection, where, ret): - if where == SSL.SSL_CB_HANDSHAKE_DONE and ret == 1: - with self.lock: - if not self.f: - d = os.path.dirname(self.filename) - if not os.path.isdir(d): - os.makedirs(d) - self.f = open(self.filename, "ab") - self.f.write(b"\r\n") - client_random = binascii.hexlify(connection.client_random()) - masterkey = binascii.hexlify(connection.master_key()) - self.f.write(b"CLIENT_RANDOM %s %s\r\n" % (client_random, masterkey)) - self.f.flush() - - def close(self): - with self.lock: - if self.f: - self.f.close() - - @staticmethod - def create_logfun(filename): - if filename: - return SSLKeyLogger(filename) - return False - -log_ssl_key = SSLKeyLogger.create_logfun( - os.getenv("MITMPROXY_SSLKEYLOGFILE") or os.getenv("SSLKEYLOGFILE")) - - -class _FileLike(object): - BLOCKSIZE = 1024 * 32 - - def __init__(self, o): - self.o = o - self._log = None - self.first_byte_timestamp = None - - def set_descriptor(self, o): - self.o = o - - def __getattr__(self, attr): - return getattr(self.o, attr) - - def start_log(self): - """ - Starts or resets the log. - - This will store all bytes read or written. - """ - self._log = [] - - def stop_log(self): - """ - Stops the log. - """ - self._log = None - - def is_logging(self): - return self._log is not None - - def get_log(self): - """ - Returns the log as a string. - """ - if not self.is_logging(): - raise ValueError("Not logging!") - return b"".join(self._log) - - def add_log(self, v): - if self.is_logging(): - self._log.append(v) - - def reset_timestamps(self): - self.first_byte_timestamp = None - - -class Writer(_FileLike): - - def flush(self): - """ - May raise TcpDisconnect - """ - if hasattr(self.o, "flush"): - try: - self.o.flush() - except (socket.error, IOError) as v: - raise TcpDisconnect(str(v)) - - def write(self, v): - """ - May raise TcpDisconnect - """ - if v: - self.first_byte_timestamp = self.first_byte_timestamp or time.time() - try: - if hasattr(self.o, "sendall"): - self.add_log(v) - return self.o.sendall(v) - else: - r = self.o.write(v) - self.add_log(v[:r]) - return r - except (SSL.Error, socket.error) as e: - raise TcpDisconnect(str(e)) - - -class Reader(_FileLike): - - def read(self, length): - """ - If length is -1, we read until connection closes. - """ - result = b'' - start = time.time() - while length == -1 or length > 0: - if length == -1 or length > self.BLOCKSIZE: - rlen = self.BLOCKSIZE - else: - rlen = length - try: - data = self.o.read(rlen) - except SSL.ZeroReturnError: - # TLS connection was shut down cleanly - break - except (SSL.WantWriteError, SSL.WantReadError): - # From the OpenSSL docs: - # If the underlying BIO is non-blocking, SSL_read() will also return when the - # underlying BIO could not satisfy the needs of SSL_read() to continue the - # operation. In this case a call to SSL_get_error with the return value of - # SSL_read() will yield SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE. - if (time.time() - start) < self.o.gettimeout(): - time.sleep(0.1) - continue - else: - raise TcpTimeout() - except socket.timeout: - raise TcpTimeout() - except socket.error as e: - raise TcpDisconnect(str(e)) - except SSL.SysCallError as e: - if e.args == (-1, 'Unexpected EOF'): - break - raise TlsException(str(e)) - except SSL.Error as e: - raise TlsException(str(e)) - self.first_byte_timestamp = self.first_byte_timestamp or time.time() - if not data: - break - result += data - if length != -1: - length -= len(data) - self.add_log(result) - return result - - def readline(self, size=None): - result = b'' - bytes_read = 0 - while True: - if size is not None and bytes_read >= size: - break - ch = self.read(1) - bytes_read += 1 - if not ch: - break - else: - result += ch - if ch == b'\n': - break - return result - - def safe_read(self, length): - """ - Like .read, but is guaranteed to either return length bytes, or - raise an exception. - """ - result = self.read(length) - if length != -1 and len(result) != length: - if not result: - raise TcpDisconnect() - else: - raise TcpReadIncomplete( - "Expected %s bytes, got %s" % (length, len(result)) - ) - return result - - def peek(self, length): - """ - Tries to peek into the underlying file object. - - Returns: - Up to the next N bytes if peeking is successful. - - Raises: - TcpException if there was an error with the socket - TlsException if there was an error with pyOpenSSL. - NotImplementedError if the underlying file object is not a [pyOpenSSL] socket - """ - if isinstance(self.o, socket_fileobject): - try: - return self.o._sock.recv(length, socket.MSG_PEEK) - except socket.error as e: - raise TcpException(repr(e)) - elif isinstance(self.o, SSL.Connection): - try: - if tuple(int(x) for x in OpenSSL.__version__.split(".")[:2]) > (0, 15): - return self.o.recv(length, socket.MSG_PEEK) - else: - # TODO: remove once a new version is released - # Polyfill for pyOpenSSL <= 0.15.1 - # Taken from https://github.com/pyca/pyopenssl/commit/1d95dea7fea03c7c0df345a5ea30c12d8a0378d2 - buf = SSL._ffi.new("char[]", length) - result = SSL._lib.SSL_peek(self.o._ssl, buf, length) - self.o._raise_ssl_error(self.o._ssl, result) - return SSL._ffi.buffer(buf, result)[:] - except SSL.Error as e: - six.reraise(TlsException, TlsException(str(e)), sys.exc_info()[2]) - else: - raise NotImplementedError("Can only peek into (pyOpenSSL) sockets") - - -class Address(utils.Serializable): - - """ - This class wraps an IPv4/IPv6 tuple to provide named attributes and - ipv6 information. - """ - - def __init__(self, address, use_ipv6=False): - self.address = tuple(address) - self.use_ipv6 = use_ipv6 - - def get_state(self): - return { - "address": self.address, - "use_ipv6": self.use_ipv6 - } - - def set_state(self, state): - self.address = state["address"] - self.use_ipv6 = state["use_ipv6"] - - @classmethod - def from_state(cls, state): - return Address(**state) - - @classmethod - def wrap(cls, t): - if isinstance(t, cls): - return t - else: - return cls(t) - - def __call__(self): - return self.address - - @property - def host(self): - return self.address[0] - - @property - def port(self): - return self.address[1] - - @property - def use_ipv6(self): - return self.family == socket.AF_INET6 - - @use_ipv6.setter - def use_ipv6(self, b): - self.family = socket.AF_INET6 if b else socket.AF_INET - - def __repr__(self): - return "{}:{}".format(self.host, self.port) - - def __str__(self): - return str(self.address) - - def __eq__(self, other): - if not other: - return False - other = Address.wrap(other) - return (self.address, self.family) == (other.address, other.family) - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.address) ^ 42 # different hash than the tuple alone. - - -def ssl_read_select(rlist, timeout): - """ - This is a wrapper around select.select() which also works for SSL.Connections - by taking ssl_connection.pending() into account. - - Caveats: - If .pending() > 0 for any of the connections in rlist, we avoid the select syscall - and **will not include any other connections which may or may not be ready**. - - Args: - rlist: wait until ready for reading - - Returns: - subset of rlist which is ready for reading. - """ - return [ - conn for conn in rlist - if isinstance(conn, SSL.Connection) and conn.pending() > 0 - ] or select.select(rlist, (), (), timeout)[0] - - -def close_socket(sock): - """ - Does a hard close of a socket, without emitting a RST. - """ - try: - # We already indicate that we close our end. - # may raise "Transport endpoint is not connected" on Linux - sock.shutdown(socket.SHUT_WR) - - # Section 4.2.2.13 of RFC 1122 tells us that a close() with any pending - # readable data could lead to an immediate RST being sent (which is the - # case on Windows). - # http://ia600609.us.archive.org/22/items/TheUltimateSo_lingerPageOrWhyIsMyTcpNotReliable/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable.html - # - # This in turn results in the following issue: If we send an error page - # to the client and then close the socket, the RST may be received by - # the client before the error page and the users sees a connection - # error rather than the error page. Thus, we try to empty the read - # buffer on Windows first. (see - # https://github.com/mitmproxy/mitmproxy/issues/527#issuecomment-93782988) - # - - if os.name == "nt": # pragma: no cover - # We cannot rely on the shutdown()-followed-by-read()-eof technique - # proposed by the page above: Some remote machines just don't send - # a TCP FIN, which would leave us in the unfortunate situation that - # recv() would block infinitely. As a workaround, we set a timeout - # here even if we are in blocking mode. - sock.settimeout(sock.gettimeout() or 20) - - # limit at a megabyte so that we don't read infinitely - for _ in range(1024 ** 3 // 4096): - # may raise a timeout/disconnect exception. - if not sock.recv(4096): - break - - # Now we can close the other half as well. - sock.shutdown(socket.SHUT_RD) - - except socket.error: - pass - - sock.close() - - -class _Connection(object): - - rbufsize = -1 - wbufsize = -1 - - def _makefile(self): - """ - Set up .rfile and .wfile attributes from .connection - """ - # Ideally, we would use the Buffered IO in Python 3 by default. - # Unfortunately, the implementation of .peek() is broken for n>1 bytes, - # as it may just return what's left in the buffer and not all the bytes we want. - # As a workaround, we just use unbuffered sockets directly. - # https://mail.python.org/pipermail/python-dev/2009-June/089986.html - if six.PY2: - self.rfile = Reader(self.connection.makefile('rb', self.rbufsize)) - self.wfile = Writer(self.connection.makefile('wb', self.wbufsize)) - else: - self.rfile = Reader(socket.SocketIO(self.connection, "rb")) - self.wfile = Writer(socket.SocketIO(self.connection, "wb")) - - def __init__(self, connection): - if connection: - self.connection = connection - self._makefile() - else: - self.connection = None - self.rfile = None - self.wfile = None - - self.ssl_established = False - self.finished = False - - def get_current_cipher(self): - if not self.ssl_established: - return None - - name = self.connection.get_cipher_name() - bits = self.connection.get_cipher_bits() - version = self.connection.get_cipher_version() - return name, bits, version - - def finish(self): - self.finished = True - # If we have an SSL connection, wfile.close == connection.close - # (We call _FileLike.set_descriptor(conn)) - # Closing the socket is not our task, therefore we don't call close - # then. - if not isinstance(self.connection, SSL.Connection): - if not getattr(self.wfile, "closed", False): - try: - self.wfile.flush() - self.wfile.close() - except TcpDisconnect: - pass - - self.rfile.close() - else: - try: - self.connection.shutdown() - except SSL.Error: - pass - - def _create_ssl_context(self, - method=SSL_DEFAULT_METHOD, - options=SSL_DEFAULT_OPTIONS, - verify_options=SSL.VERIFY_NONE, - ca_path=None, - ca_pemfile=None, - cipher_list=None, - alpn_protos=None, - alpn_select=None, - alpn_select_callback=None, - ): - """ - Creates an SSL Context. - - :param method: One of SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD, TLSv1_1_METHOD, or TLSv1_2_METHOD - :param options: A bit field consisting of OpenSSL.SSL.OP_* values - :param verify_options: A bit field consisting of OpenSSL.SSL.VERIFY_* values - :param ca_path: Path to a directory of trusted CA certificates prepared using the c_rehash tool - :param ca_pemfile: Path to a PEM formatted trusted CA certificate - :param cipher_list: A textual OpenSSL cipher list, see https://www.openssl.org/docs/apps/ciphers.html - :rtype : SSL.Context - """ - context = SSL.Context(method) - # Options (NO_SSLv2/3) - if options is not None: - context.set_options(options) - - # Verify Options (NONE/PEER and trusted CAs) - if verify_options is not None: - def verify_cert(conn, x509, errno, err_depth, is_cert_verified): - if not is_cert_verified: - self.ssl_verification_error = dict(errno=errno, - depth=err_depth) - return is_cert_verified - - context.set_verify(verify_options, verify_cert) - if ca_path is None and ca_pemfile is None: - ca_pemfile = certifi.where() - context.load_verify_locations(ca_pemfile, ca_path) - - # Workaround for - # https://github.com/pyca/pyopenssl/issues/190 - # https://github.com/mitmproxy/mitmproxy/issues/472 - # Options already set before are not cleared. - context.set_mode(SSL._lib.SSL_MODE_AUTO_RETRY) - - # Cipher List - if cipher_list: - try: - context.set_cipher_list(cipher_list) - - # TODO: maybe change this to with newer pyOpenSSL APIs - context.set_tmp_ecdh(OpenSSL.crypto.get_elliptic_curve('prime256v1')) - except SSL.Error as v: - raise TlsException("SSL cipher specification error: %s" % str(v)) - - # SSLKEYLOGFILE - if log_ssl_key: - context.set_info_callback(log_ssl_key) - - if HAS_ALPN: - if alpn_protos is not None: - # advertise application layer protocols - context.set_alpn_protos(alpn_protos) - elif alpn_select is not None and alpn_select_callback is None: - # select application layer protocol - def alpn_select_callback(conn_, options): - if alpn_select in options: - return bytes(alpn_select) - else: # pragma no cover - return options[0] - context.set_alpn_select_callback(alpn_select_callback) - elif alpn_select_callback is not None and alpn_select is None: - context.set_alpn_select_callback(alpn_select_callback) - elif alpn_select_callback is not None and alpn_select is not None: - raise TlsException("ALPN error: only define alpn_select (string) OR alpn_select_callback (method).") - - return context - - -class TCPClient(_Connection): - - def __init__(self, address, source_address=None): - super(TCPClient, self).__init__(None) - self.address = address - self.source_address = source_address - self.cert = None - self.ssl_verification_error = None - self.sni = None - - @property - def address(self): - return self.__address - - @address.setter - def address(self, address): - if address: - self.__address = Address.wrap(address) - else: - self.__address = None - - @property - def source_address(self): - return self.__source_address - - @source_address.setter - def source_address(self, source_address): - if source_address: - self.__source_address = Address.wrap(source_address) - else: - self.__source_address = None - - def close(self): - # Make sure to close the real socket, not the SSL proxy. - # OpenSSL is really good at screwing up, i.e. when trying to recv from a failed connection, - # it tries to renegotiate... - if isinstance(self.connection, SSL.Connection): - close_socket(self.connection._socket) - else: - close_socket(self.connection) - - def create_ssl_context(self, cert=None, alpn_protos=None, **sslctx_kwargs): - context = self._create_ssl_context( - alpn_protos=alpn_protos, - **sslctx_kwargs) - # Client Certs - if cert: - try: - context.use_privatekey_file(cert) - context.use_certificate_file(cert) - except SSL.Error as v: - raise TlsException("SSL client certificate error: %s" % str(v)) - return context - - def convert_to_ssl(self, sni=None, alpn_protos=None, **sslctx_kwargs): - """ - cert: Path to a file containing both client cert and private key. - - options: A bit field consisting of OpenSSL.SSL.OP_* values - verify_options: A bit field consisting of OpenSSL.SSL.VERIFY_* values - ca_path: Path to a directory of trusted CA certificates prepared using the c_rehash tool - ca_pemfile: Path to a PEM formatted trusted CA certificate - """ - verification_mode = sslctx_kwargs.get('verify_options', None) - if verification_mode == SSL.VERIFY_PEER and not sni: - raise TlsException("Cannot validate certificate hostname without SNI") - - context = self.create_ssl_context( - alpn_protos=alpn_protos, - **sslctx_kwargs - ) - self.connection = SSL.Connection(context, self.connection) - if sni: - self.sni = sni - self.connection.set_tlsext_host_name(sni) - self.connection.set_connect_state() - try: - self.connection.do_handshake() - except SSL.Error as v: - if self.ssl_verification_error: - raise InvalidCertificateException("SSL handshake error: %s" % repr(v)) - else: - raise TlsException("SSL handshake error: %s" % repr(v)) - else: - # Fix for pre v1.0 OpenSSL, which doesn't throw an exception on - # certificate validation failure - if verification_mode == SSL.VERIFY_PEER and self.ssl_verification_error is not None: - raise InvalidCertificateException("SSL handshake error: certificate verify failed") - - self.cert = certutils.SSLCert(self.connection.get_peer_certificate()) - - # Validate TLS Hostname - try: - crt = dict( - subjectAltName=[("DNS", x.decode("ascii", "strict")) for x in self.cert.altnames] - ) - if self.cert.cn: - crt["subject"] = [[["commonName", self.cert.cn.decode("ascii", "strict")]]] - if sni: - hostname = sni.decode("ascii", "strict") - else: - hostname = "no-hostname" - ssl_match_hostname.match_hostname(crt, hostname) - except (ValueError, ssl_match_hostname.CertificateError) as e: - self.ssl_verification_error = dict(depth=0, errno="Invalid Hostname") - if verification_mode == SSL.VERIFY_PEER: - raise InvalidCertificateException("Presented certificate for {} is not valid: {}".format(sni, str(e))) - - self.ssl_established = True - self.rfile.set_descriptor(self.connection) - self.wfile.set_descriptor(self.connection) - - def connect(self): - try: - connection = socket.socket(self.address.family, socket.SOCK_STREAM) - if self.source_address: - connection.bind(self.source_address()) - connection.connect(self.address()) - if not self.source_address: - self.source_address = Address(connection.getsockname()) - except (socket.error, IOError) as err: - raise TcpException( - 'Error connecting to "%s": %s' % - (self.address.host, err)) - self.connection = connection - self._makefile() - - def settimeout(self, n): - self.connection.settimeout(n) - - def gettimeout(self): - return self.connection.gettimeout() - - def get_alpn_proto_negotiated(self): - if HAS_ALPN and self.ssl_established: - return self.connection.get_alpn_proto_negotiated() - else: - return b"" - - -class BaseHandler(_Connection): - - """ - The instantiator is expected to call the handle() and finish() methods. - """ - - def __init__(self, connection, address, server): - super(BaseHandler, self).__init__(connection) - self.address = Address.wrap(address) - self.server = server - self.clientcert = None - - def create_ssl_context(self, - cert, key, - handle_sni=None, - request_client_cert=None, - chain_file=None, - dhparams=None, - **sslctx_kwargs): - """ - cert: A certutils.SSLCert object or the path to a certificate - chain file. - - handle_sni: SNI handler, should take a connection object. Server - name can be retrieved like this: - - connection.get_servername() - - And you can specify the connection keys as follows: - - new_context = Context(TLSv1_METHOD) - new_context.use_privatekey(key) - new_context.use_certificate(cert) - connection.set_context(new_context) - - The request_client_cert argument requires some explanation. We're - supposed to be able to do this with no negative effects - if the - client has no cert to present, we're notified and proceed as usual. - Unfortunately, Android seems to have a bug (tested on 4.2.2) - when - an Android client is asked to present a certificate it does not - have, it hangs up, which is frankly bogus. Some time down the track - we may be able to make the proper behaviour the default again, but - until then we're conservative. - """ - - context = self._create_ssl_context(**sslctx_kwargs) - - context.use_privatekey(key) - if isinstance(cert, certutils.SSLCert): - context.use_certificate(cert.x509) - else: - context.use_certificate_chain_file(cert) - - if handle_sni: - # SNI callback happens during do_handshake() - context.set_tlsext_servername_callback(handle_sni) - - if request_client_cert: - def save_cert(conn_, cert, errno_, depth_, preverify_ok_): - self.clientcert = certutils.SSLCert(cert) - # Return true to prevent cert verification error - return True - context.set_verify(SSL.VERIFY_PEER, save_cert) - - # Cert Verify - if chain_file: - context.load_verify_locations(chain_file) - - if dhparams: - SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) - - return context - - def convert_to_ssl(self, cert, key, **sslctx_kwargs): - """ - Convert connection to SSL. - For a list of parameters, see BaseHandler._create_ssl_context(...) - """ - - context = self.create_ssl_context( - cert, - key, - **sslctx_kwargs) - self.connection = SSL.Connection(context, self.connection) - self.connection.set_accept_state() - try: - self.connection.do_handshake() - except SSL.Error as v: - raise TlsException("SSL handshake error: %s" % repr(v)) - self.ssl_established = True - self.rfile.set_descriptor(self.connection) - self.wfile.set_descriptor(self.connection) - - def handle(self): # pragma: no cover - raise NotImplementedError - - def settimeout(self, n): - self.connection.settimeout(n) - - def get_alpn_proto_negotiated(self): - if HAS_ALPN and self.ssl_established: - return self.connection.get_alpn_proto_negotiated() - else: - return b"" - - -class TCPServer(object): - request_queue_size = 20 - - def __init__(self, address): - self.address = Address.wrap(address) - self.__is_shut_down = threading.Event() - self.__shutdown_request = False - self.socket = socket.socket(self.address.family, socket.SOCK_STREAM) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.bind(self.address()) - self.address = Address.wrap(self.socket.getsockname()) - self.socket.listen(self.request_queue_size) - - def connection_thread(self, connection, client_address): - client_address = Address(client_address) - try: - self.handle_client_connection(connection, client_address) - except: - self.handle_error(connection, client_address) - finally: - close_socket(connection) - - def serve_forever(self, poll_interval=0.1): - self.__is_shut_down.clear() - try: - while not self.__shutdown_request: - try: - r, w_, e_ = select.select( - [self.socket], [], [], poll_interval) - except select.error as ex: # pragma: no cover - if ex[0] == EINTR: - continue - else: - raise - if self.socket in r: - connection, client_address = self.socket.accept() - t = threading.Thread( - target=self.connection_thread, - args=(connection, client_address), - name="ConnectionThread (%s:%s -> %s:%s)" % - (client_address[0], client_address[1], - self.address.host, self.address.port) - ) - t.setDaemon(1) - try: - t.start() - except threading.ThreadError: - self.handle_error(connection, Address(client_address)) - connection.close() - finally: - self.__shutdown_request = False - self.__is_shut_down.set() - - def shutdown(self): - self.__shutdown_request = True - self.__is_shut_down.wait() - self.socket.close() - self.handle_shutdown() - - def handle_error(self, connection_, client_address, fp=sys.stderr): - """ - Called when handle_client_connection raises an exception. - """ - # If a thread has persisted after interpreter exit, the module might be - # none. - if traceback: - exc = six.text_type(traceback.format_exc()) - print(u'-' * 40, file=fp) - print( - u"Error in processing of request from %s" % repr(client_address), file=fp) - print(exc, file=fp) - print(u'-' * 40, file=fp) - - def handle_client_connection(self, conn, client_address): # pragma: no cover - """ - Called after client connection. - """ - raise NotImplementedError - - def handle_shutdown(self): - """ - Called after server shutdown. - """ diff --git a/netlib/netlib/tutils.py b/netlib/netlib/tutils.py deleted file mode 100644 index f6ce8e0a..00000000 --- a/netlib/netlib/tutils.py +++ /dev/null @@ -1,133 +0,0 @@ -from io import BytesIO -import tempfile -import os -import time -import shutil -from contextlib import contextmanager -import six -import sys - -from . import utils, tcp -from .http import Request, Response, Headers - - -def treader(bytes): - """ - Construct a tcp.Read object from bytes. - """ - fp = BytesIO(bytes) - return tcp.Reader(fp) - - -@contextmanager -def tmpdir(*args, **kwargs): - orig_workdir = os.getcwd() - temp_workdir = tempfile.mkdtemp(*args, **kwargs) - os.chdir(temp_workdir) - - yield temp_workdir - - os.chdir(orig_workdir) - shutil.rmtree(temp_workdir) - - -def _check_exception(expected, actual, exc_tb): - if isinstance(expected, six.string_types): - if expected.lower() not in str(actual).lower(): - six.reraise(AssertionError, AssertionError( - "Expected %s, but caught %s" % ( - repr(expected), repr(actual) - ) - ), exc_tb) - else: - if not isinstance(actual, expected): - six.reraise(AssertionError, AssertionError( - "Expected %s, but caught %s %s" % ( - expected.__name__, actual.__class__.__name__, repr(actual) - ) - ), exc_tb) - - -def raises(expected_exception, obj=None, *args, **kwargs): - """ - Assert that a callable raises a specified exception. - - :exc An exception class or a string. If a class, assert that an - exception of this type is raised. If a string, assert that the string - occurs in the string representation of the exception, based on a - case-insenstivie match. - - :obj A callable object. - - :args Arguments to be passsed to the callable. - - :kwargs Arguments to be passed to the callable. - """ - if obj is None: - return RaisesContext(expected_exception) - else: - try: - ret = obj(*args, **kwargs) - except Exception as actual: - _check_exception(expected_exception, actual, sys.exc_info()[2]) - else: - raise AssertionError("No exception raised. Return value: {}".format(ret)) - - -class RaisesContext(object): - def __init__(self, expected_exception): - self.expected_exception = expected_exception - - def __enter__(self): - return - - def __exit__(self, exc_type, exc_val, exc_tb): - if not exc_type: - raise AssertionError("No exception raised.") - else: - _check_exception(self.expected_exception, exc_val, exc_tb) - return True - - -test_data = utils.Data(__name__) -# FIXME: Temporary workaround during repo merge. -import os -test_data.dirname = os.path.join(test_data.dirname,"..","..","test","netlib") - - -def treq(**kwargs): - """ - Returns: - netlib.http.Request - """ - default = dict( - first_line_format="relative", - method=b"GET", - scheme=b"http", - host=b"address", - port=22, - path=b"/path", - http_version=b"HTTP/1.1", - headers=Headers(header="qvalue", content_length="7"), - content=b"content" - ) - default.update(kwargs) - return Request(**default) - - -def tresp(**kwargs): - """ - Returns: - netlib.http.Response - """ - default = dict( - http_version=b"HTTP/1.1", - status_code=200, - reason=b"OK", - headers=Headers(header_response="svalue", content_length="7"), - content=b"message", - timestamp_start=time.time(), - timestamp_end=time.time(), - ) - default.update(kwargs) - return Response(**default) diff --git a/netlib/netlib/utils.py b/netlib/netlib/utils.py deleted file mode 100644 index f7bb5c4b..00000000 --- a/netlib/netlib/utils.py +++ /dev/null @@ -1,418 +0,0 @@ -from __future__ import absolute_import, print_function, division -import os.path -import re -import codecs -import unicodedata -from abc import ABCMeta, abstractmethod -import importlib -import inspect - -import six - -from six.moves import urllib -import hyperframe - - -@six.add_metaclass(ABCMeta) -class Serializable(object): - """ - Abstract Base Class that defines an API to save an object's state and restore it later on. - """ - - @classmethod - @abstractmethod - def from_state(cls, state): - """ - Create a new object from the given state. - """ - raise NotImplementedError() - - @abstractmethod - def get_state(self): - """ - Retrieve object state. - """ - raise NotImplementedError() - - @abstractmethod - def set_state(self, state): - """ - Set object state to the given state. - """ - raise NotImplementedError() - - -def always_bytes(unicode_or_bytes, *encode_args): - if isinstance(unicode_or_bytes, six.text_type): - return unicode_or_bytes.encode(*encode_args) - return unicode_or_bytes - - -def always_byte_args(*encode_args): - """Decorator that transparently encodes all arguments passed as unicode""" - def decorator(fun): - def _fun(*args, **kwargs): - args = [always_bytes(arg, *encode_args) for arg in args] - kwargs = {k: always_bytes(v, *encode_args) for k, v in six.iteritems(kwargs)} - return fun(*args, **kwargs) - return _fun - return decorator - - -def native(s, *encoding_opts): - """ - Convert :py:class:`bytes` or :py:class:`unicode` to the native - :py:class:`str` type, using latin1 encoding if conversion is necessary. - - https://www.python.org/dev/peps/pep-3333/#a-note-on-string-types - """ - if not isinstance(s, (six.binary_type, six.text_type)): - raise TypeError("%r is neither bytes nor unicode" % s) - if six.PY3: - if isinstance(s, six.binary_type): - return s.decode(*encoding_opts) - else: - if isinstance(s, six.text_type): - return s.encode(*encoding_opts) - return s - - -def isascii(bytes): - try: - bytes.decode("ascii") - except ValueError: - return False - return True - - -def clean_bin(s, keep_spacing=True): - """ - Cleans binary data to make it safe to display. - - Args: - keep_spacing: If False, tabs and newlines will also be replaced. - """ - if isinstance(s, six.text_type): - if keep_spacing: - keep = u" \n\r\t" - else: - keep = u" " - return u"".join( - ch if (unicodedata.category(ch)[0] not in "CZ" or ch in keep) else u"." - for ch in s - ) - else: - if keep_spacing: - keep = (9, 10, 13) # \t, \n, \r, - else: - keep = () - return b"".join( - six.int2byte(ch) if (31 < ch < 127 or ch in keep) else b"." - for ch in six.iterbytes(s) - ) - - -def hexdump(s): - """ - Returns: - A generator of (offset, hex, str) tuples - """ - for i in range(0, len(s), 16): - offset = "{:0=10x}".format(i).encode() - part = s[i:i + 16] - x = b" ".join("{:0=2x}".format(i).encode() for i in six.iterbytes(part)) - x = x.ljust(47) # 16*2 + 15 - yield (offset, x, clean_bin(part, False)) - - -def setbit(byte, offset, value): - """ - Set a bit in a byte to 1 if value is truthy, 0 if not. - """ - if value: - return byte | (1 << offset) - else: - return byte & ~(1 << offset) - - -def getbit(byte, offset): - mask = 1 << offset - return bool(byte & mask) - - -class BiDi(object): - - """ - A wee utility class for keeping bi-directional mappings, like field - constants in protocols. Names are attributes on the object, dict-like - access maps values to names: - - CONST = BiDi(a=1, b=2) - assert CONST.a == 1 - assert CONST.get_name(1) == "a" - """ - - def __init__(self, **kwargs): - self.names = kwargs - self.values = {} - for k, v in kwargs.items(): - self.values[v] = k - if len(self.names) != len(self.values): - raise ValueError("Duplicate values not allowed.") - - def __getattr__(self, k): - if k in self.names: - return self.names[k] - raise AttributeError("No such attribute: %s", k) - - def get_name(self, n, default=None): - return self.values.get(n, default) - - -def pretty_size(size): - suffixes = [ - ("B", 2 ** 10), - ("kB", 2 ** 20), - ("MB", 2 ** 30), - ] - for suf, lim in suffixes: - if size >= lim: - continue - else: - x = round(size / float(lim / 2 ** 10), 2) - if x == int(x): - x = int(x) - return str(x) + suf - - -class Data(object): - - def __init__(self, name): - m = importlib.import_module(name) - dirname = os.path.dirname(inspect.getsourcefile(m)) - self.dirname = os.path.abspath(dirname) - - def path(self, path): - """ - Returns a path to the package data housed at 'path' under this - module.Path can be a path to a file, or to a directory. - - This function will raise ValueError if the path does not exist. - """ - fullpath = os.path.join(self.dirname, path) - if not os.path.exists(fullpath): - raise ValueError("dataPath: %s does not exist." % fullpath) - return fullpath - - -_label_valid = re.compile(b"(?!-)[A-Z\d-]{1,63}(? 255: - return False - if host[-1] == b".": - host = host[:-1] - return all(_label_valid.match(x) for x in host.split(b".")) - - -def is_valid_port(port): - return 0 <= port <= 65535 - - -# PY2 workaround -def decode_parse_result(result, enc): - if hasattr(result, "decode"): - return result.decode(enc) - else: - return urllib.parse.ParseResult(*[x.decode(enc) for x in result]) - - -# PY2 workaround -def encode_parse_result(result, enc): - if hasattr(result, "encode"): - return result.encode(enc) - else: - return urllib.parse.ParseResult(*[x.encode(enc) for x in result]) - - -def parse_url(url): - """ - URL-parsing function that checks that - - port is an integer 0-65535 - - host is a valid IDNA-encoded hostname with no null-bytes - - path is valid ASCII - - Args: - A URL (as bytes or as unicode) - - Returns: - A (scheme, host, port, path) tuple - - Raises: - ValueError, if the URL is not properly formatted. - """ - parsed = urllib.parse.urlparse(url) - - if not parsed.hostname: - raise ValueError("No hostname given") - - if isinstance(url, six.binary_type): - host = parsed.hostname - - # this should not raise a ValueError, - # but we try to be very forgiving here and accept just everything. - # decode_parse_result(parsed, "ascii") - else: - host = parsed.hostname.encode("idna") - parsed = encode_parse_result(parsed, "ascii") - - port = parsed.port - if not port: - port = 443 if parsed.scheme == b"https" else 80 - - full_path = urllib.parse.urlunparse( - (b"", b"", parsed.path, parsed.params, parsed.query, parsed.fragment) - ) - if not full_path.startswith(b"/"): - full_path = b"/" + full_path - - if not is_valid_host(host): - raise ValueError("Invalid Host") - if not is_valid_port(port): - raise ValueError("Invalid Port") - - return parsed.scheme, host, port, full_path - - -def get_header_tokens(headers, key): - """ - Retrieve all tokens for a header key. A number of different headers - follow a pattern where each header line can containe comma-separated - tokens, and headers can be set multiple times. - """ - if key not in headers: - return [] - tokens = headers[key].split(",") - return [token.strip() for token in tokens] - - -def hostport(scheme, host, port): - """ - Returns the host component, with a port specifcation if needed. - """ - if (port, scheme) in [(80, "http"), (443, "https"), (80, b"http"), (443, b"https")]: - return host - else: - if isinstance(host, six.binary_type): - return b"%s:%d" % (host, port) - else: - return "%s:%d" % (host, port) - - -def unparse_url(scheme, host, port, path=""): - """ - Returns a URL string, constructed from the specified components. - - Args: - All args must be str. - """ - return "%s://%s%s" % (scheme, hostport(scheme, host, port), path) - - -def urlencode(s): - """ - Takes a list of (key, value) tuples and returns a urlencoded string. - """ - s = [tuple(i) for i in s] - return urllib.parse.urlencode(s, False) - - -def urldecode(s): - """ - Takes a urlencoded string and returns a list of (key, value) tuples. - """ - return urllib.parse.parse_qsl(s, keep_blank_values=True) - - -def parse_content_type(c): - """ - A simple parser for content-type values. Returns a (type, subtype, - parameters) tuple, where type and subtype are strings, and parameters - is a dict. If the string could not be parsed, return None. - - E.g. the following string: - - text/html; charset=UTF-8 - - Returns: - - ("text", "html", {"charset": "UTF-8"}) - """ - parts = c.split(";", 1) - ts = parts[0].split("/", 1) - if len(ts) != 2: - return None - d = {} - if len(parts) == 2: - for i in parts[1].split(";"): - clause = i.split("=", 1) - if len(clause) == 2: - d[clause[0].strip()] = clause[1].strip() - return ts[0].lower(), ts[1].lower(), d - - -def multipartdecode(headers, content): - """ - Takes a multipart boundary encoded string and returns list of (key, value) tuples. - """ - v = headers.get("content-type") - if v: - v = parse_content_type(v) - if not v: - return [] - try: - boundary = v[2]["boundary"].encode("ascii") - except (KeyError, UnicodeError): - return [] - - rx = re.compile(br'\bname="([^"]+)"') - r = [] - - for i in content.split(b"--" + boundary): - parts = i.splitlines() - if len(parts) > 1 and parts[0][0:2] != b"--": - match = rx.search(parts[1]) - if match: - key = match.group(1) - value = b"".join(parts[3 + parts[2:].index(b""):]) - r.append((key, value)) - return r - return [] - - -def http2_read_raw_frame(rfile): - header = rfile.safe_read(9) - length = int(codecs.encode(header[:3], 'hex_codec'), 16) - - if length == 4740180: - raise ValueError("Length field looks more like HTTP/1.1: %s" % rfile.peek(20)) - - body = rfile.safe_read(length) - return [header, body] - -def http2_read_frame(rfile): - header, body = http2_read_raw_frame(rfile) - frame, length = hyperframe.frame.Frame.parse_frame_header(header) - frame.parse_body(memoryview(body)) - return frame diff --git a/netlib/netlib/version.py b/netlib/netlib/version.py deleted file mode 100644 index 379fee0f..00000000 --- a/netlib/netlib/version.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import (absolute_import, print_function, division) - -IVERSION = (0, 17) -VERSION = ".".join(str(i) for i in IVERSION) -NAME = "netlib" -NAMEVERSION = NAME + " " + VERSION diff --git a/netlib/netlib/version_check.py b/netlib/netlib/version_check.py deleted file mode 100644 index 9cf27eea..00000000 --- a/netlib/netlib/version_check.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Having installed a wrong version of pyOpenSSL or netlib is unfortunately a -very common source of error. Check before every start that both versions -are somewhat okay. -""" -from __future__ import division, absolute_import, print_function -import sys -import inspect -import os.path -import six - -import OpenSSL -from . import version - -PYOPENSSL_MIN_VERSION = (0, 15) - - -def check_mitmproxy_version(mitmproxy_version, fp=sys.stderr): - # We don't introduce backward-incompatible changes in patch versions. Only - # consider major and minor version. - if version.IVERSION[:2] != mitmproxy_version[:2]: - print( - u"You are using mitmproxy %s with netlib %s. " - u"Most likely, that won't work - please upgrade!" % ( - mitmproxy_version, version.VERSION - ), - file=fp - ) - sys.exit(1) - - -def check_pyopenssl_version(min_version=PYOPENSSL_MIN_VERSION, fp=sys.stderr): - min_version_str = u".".join(six.text_type(x) for x in min_version) - try: - v = tuple(int(x) for x in OpenSSL.__version__.split(".")[:2]) - except ValueError: - print( - u"Cannot parse pyOpenSSL version: {}" - u"mitmproxy requires pyOpenSSL {} or greater.".format( - OpenSSL.__version__, min_version_str - ), - file=fp - ) - return - if v < min_version: - print( - u"You are using an outdated version of pyOpenSSL: " - u"mitmproxy requires pyOpenSSL {} or greater.".format(min_version_str), - file=fp - ) - # Some users apparently have multiple versions of pyOpenSSL installed. - # Report which one we got. - pyopenssl_path = os.path.dirname(inspect.getfile(OpenSSL)) - print( - u"Your pyOpenSSL {} installation is located at {}".format( - OpenSSL.__version__, pyopenssl_path - ), - file=fp - ) - sys.exit(1) diff --git a/netlib/netlib/websockets/__init__.py b/netlib/netlib/websockets/__init__.py deleted file mode 100644 index 1c143919..00000000 --- a/netlib/netlib/websockets/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .frame import * -from .protocol import * diff --git a/netlib/netlib/websockets/frame.py b/netlib/netlib/websockets/frame.py deleted file mode 100644 index fce2c9d3..00000000 --- a/netlib/netlib/websockets/frame.py +++ /dev/null @@ -1,316 +0,0 @@ -from __future__ import absolute_import -import os -import struct -import io -import warnings - -import six - -from .protocol import Masker -from netlib import tcp -from netlib import utils - - -MAX_16_BIT_INT = (1 << 16) -MAX_64_BIT_INT = (1 << 64) - -DEFAULT=object() - -OPCODE = utils.BiDi( - CONTINUE=0x00, - TEXT=0x01, - BINARY=0x02, - CLOSE=0x08, - PING=0x09, - PONG=0x0a -) - - -class FrameHeader(object): - - def __init__( - self, - opcode=OPCODE.TEXT, - payload_length=0, - fin=False, - rsv1=False, - rsv2=False, - rsv3=False, - masking_key=DEFAULT, - mask=DEFAULT, - length_code=DEFAULT - ): - if not 0 <= opcode < 2 ** 4: - raise ValueError("opcode must be 0-16") - self.opcode = opcode - self.payload_length = payload_length - self.fin = fin - self.rsv1 = rsv1 - self.rsv2 = rsv2 - self.rsv3 = rsv3 - - if length_code is DEFAULT: - self.length_code = self._make_length_code(self.payload_length) - else: - self.length_code = length_code - - if mask is DEFAULT and masking_key is DEFAULT: - self.mask = False - self.masking_key = b"" - elif mask is DEFAULT: - self.mask = 1 - self.masking_key = masking_key - elif masking_key is DEFAULT: - self.mask = mask - self.masking_key = os.urandom(4) - else: - self.mask = mask - self.masking_key = masking_key - - if self.masking_key and len(self.masking_key) != 4: - raise ValueError("Masking key must be 4 bytes.") - - @classmethod - def _make_length_code(self, length): - """ - A websockets frame contains an initial length_code, and an optional - extended length code to represent the actual length if length code is - larger than 125 - """ - if length <= 125: - return length - elif length >= 126 and length <= 65535: - return 126 - else: - return 127 - - def __repr__(self): - vals = [ - "ws frame:", - OPCODE.get_name(self.opcode, hex(self.opcode)).lower() - ] - flags = [] - for i in ["fin", "rsv1", "rsv2", "rsv3", "mask"]: - if getattr(self, i): - flags.append(i) - if flags: - vals.extend([":", "|".join(flags)]) - if self.masking_key: - vals.append(":key=%s" % repr(self.masking_key)) - if self.payload_length: - vals.append(" %s" % utils.pretty_size(self.payload_length)) - return "".join(vals) - - def human_readable(self): - warnings.warn("FrameHeader.to_bytes is deprecated, use bytes(frame_header) instead.", DeprecationWarning) - return repr(self) - - def __bytes__(self): - first_byte = utils.setbit(0, 7, self.fin) - first_byte = utils.setbit(first_byte, 6, self.rsv1) - first_byte = utils.setbit(first_byte, 5, self.rsv2) - first_byte = utils.setbit(first_byte, 4, self.rsv3) - first_byte = first_byte | self.opcode - - second_byte = utils.setbit(self.length_code, 7, self.mask) - - b = six.int2byte(first_byte) + six.int2byte(second_byte) - - if self.payload_length < 126: - pass - elif self.payload_length < MAX_16_BIT_INT: - # '!H' pack as 16 bit unsigned short - # add 2 byte extended payload length - b += struct.pack('!H', self.payload_length) - elif self.payload_length < MAX_64_BIT_INT: - # '!Q' = pack as 64 bit unsigned long long - # add 8 bytes extended payload length - b += struct.pack('!Q', self.payload_length) - if self.masking_key: - b += self.masking_key - return b - - if six.PY2: - __str__ = __bytes__ - - def to_bytes(self): - warnings.warn("FrameHeader.to_bytes is deprecated, use bytes(frame_header) instead.", DeprecationWarning) - return bytes(self) - - @classmethod - def from_file(cls, fp): - """ - read a websockets frame header - """ - first_byte = six.byte2int(fp.safe_read(1)) - second_byte = six.byte2int(fp.safe_read(1)) - - fin = utils.getbit(first_byte, 7) - rsv1 = utils.getbit(first_byte, 6) - rsv2 = utils.getbit(first_byte, 5) - rsv3 = utils.getbit(first_byte, 4) - # grab right-most 4 bits - opcode = first_byte & 15 - mask_bit = utils.getbit(second_byte, 7) - # grab the next 7 bits - length_code = second_byte & 127 - - # payload_lengthy > 125 indicates you need to read more bytes - # to get the actual payload length - if length_code <= 125: - payload_length = length_code - elif length_code == 126: - payload_length, = struct.unpack("!H", fp.safe_read(2)) - elif length_code == 127: - payload_length, = struct.unpack("!Q", fp.safe_read(8)) - - # masking key only present if mask bit set - if mask_bit == 1: - masking_key = fp.safe_read(4) - else: - masking_key = None - - return cls( - fin=fin, - rsv1=rsv1, - rsv2=rsv2, - rsv3=rsv3, - opcode=opcode, - mask=mask_bit, - length_code=length_code, - payload_length=payload_length, - masking_key=masking_key, - ) - - def __eq__(self, other): - if isinstance(other, FrameHeader): - return bytes(self) == bytes(other) - return False - - -class Frame(object): - - """ - Represents one websockets frame. - Constructor takes human readable forms of the frame components - from_bytes() is also avaliable. - - WebSockets Frame as defined in RFC6455 - - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-------+-+-------------+-------------------------------+ - |F|R|R|R| opcode|M| Payload len | Extended payload length | - |I|S|S|S| (4) |A| (7) | (16/64) | - |N|V|V|V| |S| | (if payload len==126/127) | - | |1|2|3| |K| | | - +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + - | Extended payload length continued, if payload len == 127 | - + - - - - - - - - - - - - - - - +-------------------------------+ - | |Masking-key, if MASK set to 1 | - +-------------------------------+-------------------------------+ - | Masking-key (continued) | Payload Data | - +-------------------------------- - - - - - - - - - - - - - - - + - : Payload Data continued ... : - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - | Payload Data continued ... | - +---------------------------------------------------------------+ - """ - - def __init__(self, payload=b"", **kwargs): - self.payload = payload - kwargs["payload_length"] = kwargs.get("payload_length", len(payload)) - self.header = FrameHeader(**kwargs) - - @classmethod - def default(cls, message, from_client=False): - """ - Construct a basic websocket frame from some default values. - Creates a non-fragmented text frame. - """ - if from_client: - mask_bit = 1 - masking_key = os.urandom(4) - else: - mask_bit = 0 - masking_key = None - - return cls( - message, - fin=1, # final frame - opcode=OPCODE.TEXT, # text - mask=mask_bit, - masking_key=masking_key, - ) - - @classmethod - def from_bytes(cls, bytestring): - """ - Construct a websocket frame from an in-memory bytestring - to construct a frame from a stream of bytes, use from_file() directly - """ - return cls.from_file(tcp.Reader(io.BytesIO(bytestring))) - - def __repr__(self): - ret = repr(self.header) - if self.payload: - ret = ret + "\nPayload:\n" + utils.clean_bin(self.payload).decode("ascii") - return ret - - def human_readable(self): - warnings.warn("Frame.to_bytes is deprecated, use bytes(frame) instead.", DeprecationWarning) - return repr(self) - - def __bytes__(self): - """ - Serialize the frame to wire format. Returns a string. - """ - b = bytes(self.header) - if self.header.masking_key: - b += Masker(self.header.masking_key)(self.payload) - else: - b += self.payload - return b - - if six.PY2: - __str__ = __bytes__ - - def to_bytes(self): - warnings.warn("FrameHeader.to_bytes is deprecated, use bytes(frame_header) instead.", DeprecationWarning) - return bytes(self) - - def to_file(self, writer): - warnings.warn("Frame.to_file is deprecated, use wfile.write(bytes(frame)) instead.", DeprecationWarning) - writer.write(bytes(self)) - writer.flush() - - @classmethod - def from_file(cls, fp): - """ - read a websockets frame sent by a server or client - - fp is a "file like" object that could be backed by a network - stream or a disk or an in memory stream reader - """ - header = FrameHeader.from_file(fp) - payload = fp.safe_read(header.payload_length) - - if header.mask == 1 and header.masking_key: - payload = Masker(header.masking_key)(payload) - - return cls( - payload, - fin=header.fin, - opcode=header.opcode, - mask=header.mask, - payload_length=header.payload_length, - masking_key=header.masking_key, - rsv1=header.rsv1, - rsv2=header.rsv2, - rsv3=header.rsv3, - length_code=header.length_code - ) - - def __eq__(self, other): - if isinstance(other, Frame): - return bytes(self) == bytes(other) - return False diff --git a/netlib/netlib/websockets/protocol.py b/netlib/netlib/websockets/protocol.py deleted file mode 100644 index 1e95fa1c..00000000 --- a/netlib/netlib/websockets/protocol.py +++ /dev/null @@ -1,115 +0,0 @@ - - - -# Colleciton of utility functions that implement small portions of the RFC6455 -# WebSockets Protocol Useful for building WebSocket clients and servers. -# -# Emphassis is on readabilty, simplicity and modularity, not performance or -# completeness -# -# This is a work in progress and does not yet contain all the utilites need to -# create fully complient client/servers # -# Spec: https://tools.ietf.org/html/rfc6455 - -# The magic sha that websocket servers must know to prove they understand -# RFC6455 -from __future__ import absolute_import -import base64 -import hashlib -import os - -import binascii -import six -from ..http import Headers - -websockets_magic = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' -VERSION = "13" - - -class Masker(object): - - """ - Data sent from the server must be masked to prevent malicious clients - from sending data over the wire in predictable patterns - - Servers do not have to mask data they send to the client. - https://tools.ietf.org/html/rfc6455#section-5.3 - """ - - def __init__(self, key): - self.key = key - self.offset = 0 - - def mask(self, offset, data): - result = bytearray(data) - if six.PY2: - for i in range(len(data)): - result[i] ^= ord(self.key[offset % 4]) - offset += 1 - result = str(result) - else: - - for i in range(len(data)): - result[i] ^= self.key[offset % 4] - offset += 1 - result = bytes(result) - return result - - def __call__(self, data): - ret = self.mask(self.offset, data) - self.offset += len(ret) - return ret - - -class WebsocketsProtocol(object): - - def __init__(self): - pass - - @classmethod - def client_handshake_headers(self, key=None, version=VERSION): - """ - Create the headers for a valid HTTP upgrade request. If Key is not - specified, it is generated, and can be found in sec-websocket-key in - the returned header set. - - Returns an instance of Headers - """ - if not key: - key = base64.b64encode(os.urandom(16)).decode('ascii') - return Headers( - sec_websocket_key=key, - sec_websocket_version=version, - connection="Upgrade", - upgrade="websocket", - ) - - @classmethod - def server_handshake_headers(self, key): - """ - The server response is a valid HTTP 101 response. - """ - return Headers( - sec_websocket_accept=self.create_server_nonce(key), - connection="Upgrade", - upgrade="websocket" - ) - - - @classmethod - def check_client_handshake(self, headers): - if headers.get("upgrade") != "websocket": - return - return headers.get("sec-websocket-key") - - - @classmethod - def check_server_handshake(self, headers): - if headers.get("upgrade") != "websocket": - return - return headers.get("sec-websocket-accept") - - - @classmethod - def create_server_nonce(self, client_nonce): - return base64.b64encode(hashlib.sha1(client_nonce + websockets_magic).digest()) diff --git a/netlib/netlib/wsgi.py b/netlib/netlib/wsgi.py deleted file mode 100644 index d6dfae5d..00000000 --- a/netlib/netlib/wsgi.py +++ /dev/null @@ -1,164 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -from io import BytesIO, StringIO -import urllib -import time -import traceback - -import six -from six.moves import urllib - -from netlib.utils import always_bytes, native -from . import http, tcp - -class ClientConn(object): - - def __init__(self, address): - self.address = tcp.Address.wrap(address) - - -class Flow(object): - - def __init__(self, address, request): - self.client_conn = ClientConn(address) - self.request = request - - -class Request(object): - - def __init__(self, scheme, method, path, http_version, headers, content): - self.scheme, self.method, self.path = scheme, method, path - self.headers, self.content = headers, content - self.http_version = http_version - - -def date_time_string(): - """Return the current date and time formatted for a message header.""" - WEEKS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - MONTHS = [ - None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' - ] - now = time.time() - year, month, day, hh, mm, ss, wd, y_, z_ = time.gmtime(now) - s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( - WEEKS[wd], - day, MONTHS[month], year, - hh, mm, ss - ) - return s - - -class WSGIAdaptor(object): - - def __init__(self, app, domain, port, sversion): - self.app, self.domain, self.port, self.sversion = app, domain, port, sversion - - def make_environ(self, flow, errsoc, **extra): - path = native(flow.request.path, "latin-1") - if '?' in path: - path_info, query = native(path, "latin-1").split('?', 1) - else: - path_info = path - query = '' - environ = { - 'wsgi.version': (1, 0), - 'wsgi.url_scheme': native(flow.request.scheme, "latin-1"), - 'wsgi.input': BytesIO(flow.request.content or b""), - 'wsgi.errors': errsoc, - 'wsgi.multithread': True, - 'wsgi.multiprocess': False, - 'wsgi.run_once': False, - 'SERVER_SOFTWARE': self.sversion, - 'REQUEST_METHOD': native(flow.request.method, "latin-1"), - 'SCRIPT_NAME': '', - 'PATH_INFO': urllib.parse.unquote(path_info), - 'QUERY_STRING': query, - 'CONTENT_TYPE': native(flow.request.headers.get('Content-Type', ''), "latin-1"), - 'CONTENT_LENGTH': native(flow.request.headers.get('Content-Length', ''), "latin-1"), - 'SERVER_NAME': self.domain, - 'SERVER_PORT': str(self.port), - 'SERVER_PROTOCOL': native(flow.request.http_version, "latin-1"), - } - environ.update(extra) - if flow.client_conn.address: - environ["REMOTE_ADDR"] = native(flow.client_conn.address.host, "latin-1") - environ["REMOTE_PORT"] = flow.client_conn.address.port - - for key, value in flow.request.headers.items(): - key = 'HTTP_' + native(key, "latin-1").upper().replace('-', '_') - if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): - environ[key] = value - return environ - - def error_page(self, soc, headers_sent, s): - """ - Make a best-effort attempt to write an error page. If headers are - already sent, we just bung the error into the page. - """ - c = """ - -

    Internal Server Error

    -
    {err}"
    - - """.format(err=s).strip().encode() - - if not headers_sent: - soc.write(b"HTTP/1.1 500 Internal Server Error\r\n") - soc.write(b"Content-Type: text/html\r\n") - soc.write("Content-Length: {length}\r\n".format(length=len(c)).encode()) - soc.write(b"\r\n") - soc.write(c) - - def serve(self, request, soc, **env): - state = dict( - response_started=False, - headers_sent=False, - status=None, - headers=None - ) - - def write(data): - if not state["headers_sent"]: - soc.write("HTTP/1.1 {status}\r\n".format(status=state["status"]).encode()) - headers = state["headers"] - if 'server' not in headers: - headers["Server"] = self.sversion - if 'date' not in headers: - headers["Date"] = date_time_string() - soc.write(bytes(headers)) - soc.write(b"\r\n") - state["headers_sent"] = True - if data: - soc.write(data) - soc.flush() - - def start_response(status, headers, exc_info=None): - if exc_info: - if state["headers_sent"]: - six.reraise(*exc_info) - elif state["status"]: - raise AssertionError('Response already started') - state["status"] = status - state["headers"] = http.Headers([[always_bytes(k), always_bytes(v)] for k,v in headers]) - if exc_info: - self.error_page(soc, state["headers_sent"], traceback.format_tb(exc_info[2])) - state["headers_sent"] = True - - errs = six.BytesIO() - try: - dataiter = self.app( - self.make_environ(request, errs, **env), start_response - ) - for i in dataiter: - write(i) - if not state["headers_sent"]: - write(b"") - except Exception as e: - try: - s = traceback.format_exc() - errs.write(s.encode("utf-8", "replace")) - self.error_page(soc, state["headers_sent"], s) - except Exception: # pragma: no cover - pass - return errs.getvalue() diff --git a/netlib/odict.py b/netlib/odict.py new file mode 100644 index 00000000..1e6e381a --- /dev/null +++ b/netlib/odict.py @@ -0,0 +1,193 @@ +from __future__ import (absolute_import, print_function, division) +import re +import copy +import six + +from .utils import Serializable + + +def safe_subn(pattern, repl, target, *args, **kwargs): + """ + There are Unicode conversion problems with re.subn. We try to smooth + that over by casting the pattern and replacement to strings. We really + need a better solution that is aware of the actual content ecoding. + """ + return re.subn(str(pattern), str(repl), target, *args, **kwargs) + + +class ODict(Serializable): + + """ + A dictionary-like object for managing ordered (key, value) data. Think + about it as a convenient interface to a list of (key, value) tuples. + """ + + def __init__(self, lst=None): + self.lst = lst or [] + + def _kconv(self, s): + return s + + def __eq__(self, other): + return self.lst == other.lst + + def __ne__(self, other): + return not self.__eq__(other) + + def __iter__(self): + return self.lst.__iter__() + + def __getitem__(self, k): + """ + Returns a list of values matching key. + """ + ret = [] + k = self._kconv(k) + for i in self.lst: + if self._kconv(i[0]) == k: + ret.append(i[1]) + return ret + + def keys(self): + return list(set([self._kconv(i[0]) for i in self.lst])) + + def _filter_lst(self, k, lst): + k = self._kconv(k) + new = [] + for i in lst: + if self._kconv(i[0]) != k: + new.append(i) + return new + + def __len__(self): + """ + Total number of (key, value) pairs. + """ + return len(self.lst) + + def __setitem__(self, k, valuelist): + """ + Sets the values for key k. If there are existing values for this + key, they are cleared. + """ + if isinstance(valuelist, six.text_type) or isinstance(valuelist, six.binary_type): + raise ValueError( + "Expected list of values instead of string. " + "Example: odict[b'Host'] = [b'www.example.com']" + ) + kc = self._kconv(k) + new = [] + for i in self.lst: + if self._kconv(i[0]) == kc: + if valuelist: + new.append([k, valuelist.pop(0)]) + else: + new.append(i) + while valuelist: + new.append([k, valuelist.pop(0)]) + self.lst = new + + def __delitem__(self, k): + """ + Delete all items matching k. + """ + self.lst = self._filter_lst(k, self.lst) + + def __contains__(self, k): + k = self._kconv(k) + for i in self.lst: + if self._kconv(i[0]) == k: + return True + return False + + def add(self, key, value, prepend=False): + if prepend: + self.lst.insert(0, [key, value]) + else: + self.lst.append([key, value]) + + def get(self, k, d=None): + if k in self: + return self[k] + else: + return d + + def get_first(self, k, d=None): + if k in self: + return self[k][0] + else: + return d + + def items(self): + return self.lst[:] + + def copy(self): + """ + Returns a copy of this object. + """ + lst = copy.deepcopy(self.lst) + return self.__class__(lst) + + def extend(self, other): + """ + Add the contents of other, preserving any duplicates. + """ + self.lst.extend(other.lst) + + def __repr__(self): + return repr(self.lst) + + def in_any(self, key, value, caseless=False): + """ + Do any of the values matching key contain value? + + If caseless is true, value comparison is case-insensitive. + """ + if caseless: + value = value.lower() + for i in self[key]: + if caseless: + i = i.lower() + if value in i: + return True + return False + + def replace(self, pattern, repl, *args, **kwargs): + """ + Replaces a regular expression pattern with repl in both keys and + values. Encoded content will be decoded before replacement, and + re-encoded afterwards. + + Returns the number of replacements made. + """ + nlst, count = [], 0 + for i in self.lst: + k, c = safe_subn(pattern, repl, i[0], *args, **kwargs) + count += c + v, c = safe_subn(pattern, repl, i[1], *args, **kwargs) + count += c + nlst.append([k, v]) + self.lst = nlst + return count + + # Implement the StateObject protocol from mitmproxy + def get_state(self): + return [tuple(i) for i in self.lst] + + def set_state(self, state): + self.lst = [list(i) for i in state] + + @classmethod + def from_state(cls, state): + return cls([list(i) for i in state]) + + +class ODictCaseless(ODict): + + """ + A variant of ODict with "caseless" keys. This version _preserves_ key + case, but does not consider case when setting or getting items. + """ + + def _kconv(self, s): + return s.lower() diff --git a/netlib/setup.cfg b/netlib/setup.cfg deleted file mode 100644 index 3480374b..00000000 --- a/netlib/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 \ No newline at end of file diff --git a/netlib/setup.py b/netlib/setup.py deleted file mode 100644 index 0c9a721d..00000000 --- a/netlib/setup.py +++ /dev/null @@ -1,70 +0,0 @@ -from setuptools import setup, find_packages -from codecs import open -import os -import sys - -from netlib import version - -# Based on https://github.com/pypa/sampleproject/blob/master/setup.py -# and https://python-packaging-user-guide.readthedocs.org/ -# and https://caremad.io/2014/11/distributing-a-cffi-project/ - -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -setup( - name="netlib", - version=version.VERSION, - description="A collection of network utilities used by pathod and mitmproxy.", - long_description=long_description, - url="http://github.com/mitmproxy/netlib", - author="Aldo Cortesi", - author_email="aldo@corte.si", - license="MIT", - classifiers=[ - "License :: OSI Approved :: MIT License", - "Development Status :: 3 - Alpha", - "Operating System :: POSIX", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Internet", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: HTTP Servers", - "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Testing :: Traffic Generation", - ], - packages=find_packages(), - install_requires=[ - "pyasn1>=0.1.9, <0.2", - "pyOpenSSL>=0.15.1, <0.16", - "cryptography>=1.2.2, <1.3", - "passlib>=1.6.5, <1.7", - "hpack>=2.1.0, <3.0", - "hyperframe>=3.2.0, <4.0", - "six>=1.10.0, <1.11", - "certifi>=2015.11.20.1", # no semver here - this should always be on the last release! - "backports.ssl_match_hostname>=3.5.0.1, <3.6", - ], - extras_require={ - # Do not use a range operator here: https://bitbucket.org/pypa/setuptools/issues/380 - # Ubuntu Trusty and other still ship with setuptools < 17.1 - ':python_version == "2.7"': [ - "ipaddress>=1.0.15, <1.1", - ], - 'dev': [ - "mock>=1.3.0, <1.4", - "pytest>=2.8.7, <2.9", - "pytest-xdist>=1.14, <1.15", - "pytest-cov>=2.2.1, <2.3", - "pytest-timeout>=1.0.0, <1.1", - "coveralls>=1.1, <1.2" - ] - }, -) diff --git a/netlib/socks.py b/netlib/socks.py new file mode 100644 index 00000000..51ad1c63 --- /dev/null +++ b/netlib/socks.py @@ -0,0 +1,176 @@ +from __future__ import (absolute_import, print_function, division) +import struct +import array +import ipaddress +from . import tcp, utils + + +class SocksError(Exception): + def __init__(self, code, message): + super(SocksError, self).__init__(message) + self.code = code + + +VERSION = utils.BiDi( + SOCKS4=0x04, + SOCKS5=0x05 +) + +CMD = utils.BiDi( + CONNECT=0x01, + BIND=0x02, + UDP_ASSOCIATE=0x03 +) + +ATYP = utils.BiDi( + IPV4_ADDRESS=0x01, + DOMAINNAME=0x03, + IPV6_ADDRESS=0x04 +) + +REP = utils.BiDi( + SUCCEEDED=0x00, + GENERAL_SOCKS_SERVER_FAILURE=0x01, + CONNECTION_NOT_ALLOWED_BY_RULESET=0x02, + NETWORK_UNREACHABLE=0x03, + HOST_UNREACHABLE=0x04, + CONNECTION_REFUSED=0x05, + TTL_EXPIRED=0x06, + COMMAND_NOT_SUPPORTED=0x07, + ADDRESS_TYPE_NOT_SUPPORTED=0x08, +) + +METHOD = utils.BiDi( + NO_AUTHENTICATION_REQUIRED=0x00, + GSSAPI=0x01, + USERNAME_PASSWORD=0x02, + NO_ACCEPTABLE_METHODS=0xFF +) + + +class ClientGreeting(object): + __slots__ = ("ver", "methods") + + def __init__(self, ver, methods): + self.ver = ver + self.methods = array.array("B") + self.methods.extend(methods) + + def assert_socks5(self): + if self.ver != VERSION.SOCKS5: + if self.ver == ord("G") and len(self.methods) == ord("E"): + guess = "Probably not a SOCKS request but a regular HTTP request. " + else: + guess = "" + + raise SocksError( + REP.GENERAL_SOCKS_SERVER_FAILURE, + guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.ver + ) + + @classmethod + def from_file(cls, f, fail_early=False): + """ + :param fail_early: If true, a SocksError will be raised if the first byte does not indicate socks5. + """ + ver, nmethods = struct.unpack("!BB", f.safe_read(2)) + client_greeting = cls(ver, []) + if fail_early: + client_greeting.assert_socks5() + client_greeting.methods.fromstring(f.safe_read(nmethods)) + return client_greeting + + def to_file(self, f): + f.write(struct.pack("!BB", self.ver, len(self.methods))) + f.write(self.methods.tostring()) + + +class ServerGreeting(object): + __slots__ = ("ver", "method") + + def __init__(self, ver, method): + self.ver = ver + self.method = method + + def assert_socks5(self): + if self.ver != VERSION.SOCKS5: + if self.ver == ord("H") and self.method == ord("T"): + guess = "Probably not a SOCKS request but a regular HTTP response. " + else: + guess = "" + + raise SocksError( + REP.GENERAL_SOCKS_SERVER_FAILURE, + guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.ver + ) + + @classmethod + def from_file(cls, f): + ver, method = struct.unpack("!BB", f.safe_read(2)) + return cls(ver, method) + + def to_file(self, f): + f.write(struct.pack("!BB", self.ver, self.method)) + + +class Message(object): + __slots__ = ("ver", "msg", "atyp", "addr") + + def __init__(self, ver, msg, atyp, addr): + self.ver = ver + self.msg = msg + self.atyp = atyp + self.addr = tcp.Address.wrap(addr) + + def assert_socks5(self): + if self.ver != VERSION.SOCKS5: + raise SocksError( + REP.GENERAL_SOCKS_SERVER_FAILURE, + "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.ver + ) + + @classmethod + def from_file(cls, f): + ver, msg, rsv, atyp = struct.unpack("!BBBB", f.safe_read(4)) + if rsv != 0x00: + raise SocksError( + REP.GENERAL_SOCKS_SERVER_FAILURE, + "Socks Request: Invalid reserved byte: %s" % rsv + ) + if atyp == ATYP.IPV4_ADDRESS: + # We use tnoa here as ntop is not commonly available on Windows. + host = ipaddress.IPv4Address(f.safe_read(4)).compressed + use_ipv6 = False + elif atyp == ATYP.IPV6_ADDRESS: + host = ipaddress.IPv6Address(f.safe_read(16)).compressed + use_ipv6 = True + elif atyp == ATYP.DOMAINNAME: + length, = struct.unpack("!B", f.safe_read(1)) + host = f.safe_read(length) + if not utils.is_valid_host(host): + raise SocksError(REP.GENERAL_SOCKS_SERVER_FAILURE, "Invalid hostname: %s" % host) + host = host.decode("idna") + use_ipv6 = False + else: + raise SocksError(REP.ADDRESS_TYPE_NOT_SUPPORTED, + "Socks Request: Unknown ATYP: %s" % atyp) + + port, = struct.unpack("!H", f.safe_read(2)) + addr = tcp.Address((host, port), use_ipv6=use_ipv6) + return cls(ver, msg, atyp, addr) + + def to_file(self, f): + f.write(struct.pack("!BBBB", self.ver, self.msg, 0x00, self.atyp)) + if self.atyp == ATYP.IPV4_ADDRESS: + f.write(ipaddress.IPv4Address(self.addr.host).packed) + elif self.atyp == ATYP.IPV6_ADDRESS: + f.write(ipaddress.IPv6Address(self.addr.host).packed) + elif self.atyp == ATYP.DOMAINNAME: + f.write(struct.pack("!B", len(self.addr.host))) + f.write(self.addr.host.encode("idna")) + else: + raise SocksError( + REP.ADDRESS_TYPE_NOT_SUPPORTED, + "Unknown ATYP: %s" % self.atyp + ) + f.write(struct.pack("!H", self.addr.port)) diff --git a/netlib/tcp.py b/netlib/tcp.py new file mode 100644 index 00000000..61b41cdc --- /dev/null +++ b/netlib/tcp.py @@ -0,0 +1,911 @@ +from __future__ import (absolute_import, print_function, division) +import os +import select +import socket +import sys +import threading +import time +import traceback + +import binascii +from six.moves import range + +import certifi +from backports import ssl_match_hostname +import six +import OpenSSL +from OpenSSL import SSL + +from . import certutils, version_check, utils + +# This is a rather hackish way to make sure that +# the latest version of pyOpenSSL is actually installed. +from netlib.exceptions import InvalidCertificateException, TcpReadIncomplete, TlsException, \ + TcpTimeout, TcpDisconnect, TcpException + +version_check.check_pyopenssl_version() + +if six.PY2: + socket_fileobject = socket._fileobject +else: + socket_fileobject = socket.SocketIO + +EINTR = 4 +if os.environ.get("NO_ALPN"): + HAS_ALPN = False +else: + HAS_ALPN = OpenSSL._util.lib.Cryptography_HAS_ALPN + +# To enable all SSL methods use: SSLv23 +# then add options to disable certain methods +# https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 +SSL_BASIC_OPTIONS = ( + SSL.OP_CIPHER_SERVER_PREFERENCE +) +if hasattr(SSL, "OP_NO_COMPRESSION"): + SSL_BASIC_OPTIONS |= SSL.OP_NO_COMPRESSION + +SSL_DEFAULT_METHOD = SSL.SSLv23_METHOD +SSL_DEFAULT_OPTIONS = ( + SSL.OP_NO_SSLv2 | + SSL.OP_NO_SSLv3 | + SSL_BASIC_OPTIONS +) +if hasattr(SSL, "OP_NO_COMPRESSION"): + SSL_DEFAULT_OPTIONS |= SSL.OP_NO_COMPRESSION + +""" +Map a reasonable SSL version specification into the format OpenSSL expects. +Don't ask... +https://bugs.launchpad.net/pyopenssl/+bug/1020632/comments/3 +""" +sslversion_choices = { + "all": (SSL.SSLv23_METHOD, SSL_BASIC_OPTIONS), + # SSLv23_METHOD + NO_SSLv2 + NO_SSLv3 == TLS 1.0+ + # TLSv1_METHOD would be TLS 1.0 only + "secure": (SSL.SSLv23_METHOD, (SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 | SSL_BASIC_OPTIONS)), + "SSLv2": (SSL.SSLv2_METHOD, SSL_BASIC_OPTIONS), + "SSLv3": (SSL.SSLv3_METHOD, SSL_BASIC_OPTIONS), + "TLSv1": (SSL.TLSv1_METHOD, SSL_BASIC_OPTIONS), + "TLSv1_1": (SSL.TLSv1_1_METHOD, SSL_BASIC_OPTIONS), + "TLSv1_2": (SSL.TLSv1_2_METHOD, SSL_BASIC_OPTIONS), +} + +class SSLKeyLogger(object): + + def __init__(self, filename): + self.filename = filename + self.f = None + self.lock = threading.Lock() + + # required for functools.wraps, which pyOpenSSL uses. + __name__ = "SSLKeyLogger" + + def __call__(self, connection, where, ret): + if where == SSL.SSL_CB_HANDSHAKE_DONE and ret == 1: + with self.lock: + if not self.f: + d = os.path.dirname(self.filename) + if not os.path.isdir(d): + os.makedirs(d) + self.f = open(self.filename, "ab") + self.f.write(b"\r\n") + client_random = binascii.hexlify(connection.client_random()) + masterkey = binascii.hexlify(connection.master_key()) + self.f.write(b"CLIENT_RANDOM %s %s\r\n" % (client_random, masterkey)) + self.f.flush() + + def close(self): + with self.lock: + if self.f: + self.f.close() + + @staticmethod + def create_logfun(filename): + if filename: + return SSLKeyLogger(filename) + return False + +log_ssl_key = SSLKeyLogger.create_logfun( + os.getenv("MITMPROXY_SSLKEYLOGFILE") or os.getenv("SSLKEYLOGFILE")) + + +class _FileLike(object): + BLOCKSIZE = 1024 * 32 + + def __init__(self, o): + self.o = o + self._log = None + self.first_byte_timestamp = None + + def set_descriptor(self, o): + self.o = o + + def __getattr__(self, attr): + return getattr(self.o, attr) + + def start_log(self): + """ + Starts or resets the log. + + This will store all bytes read or written. + """ + self._log = [] + + def stop_log(self): + """ + Stops the log. + """ + self._log = None + + def is_logging(self): + return self._log is not None + + def get_log(self): + """ + Returns the log as a string. + """ + if not self.is_logging(): + raise ValueError("Not logging!") + return b"".join(self._log) + + def add_log(self, v): + if self.is_logging(): + self._log.append(v) + + def reset_timestamps(self): + self.first_byte_timestamp = None + + +class Writer(_FileLike): + + def flush(self): + """ + May raise TcpDisconnect + """ + if hasattr(self.o, "flush"): + try: + self.o.flush() + except (socket.error, IOError) as v: + raise TcpDisconnect(str(v)) + + def write(self, v): + """ + May raise TcpDisconnect + """ + if v: + self.first_byte_timestamp = self.first_byte_timestamp or time.time() + try: + if hasattr(self.o, "sendall"): + self.add_log(v) + return self.o.sendall(v) + else: + r = self.o.write(v) + self.add_log(v[:r]) + return r + except (SSL.Error, socket.error) as e: + raise TcpDisconnect(str(e)) + + +class Reader(_FileLike): + + def read(self, length): + """ + If length is -1, we read until connection closes. + """ + result = b'' + start = time.time() + while length == -1 or length > 0: + if length == -1 or length > self.BLOCKSIZE: + rlen = self.BLOCKSIZE + else: + rlen = length + try: + data = self.o.read(rlen) + except SSL.ZeroReturnError: + # TLS connection was shut down cleanly + break + except (SSL.WantWriteError, SSL.WantReadError): + # From the OpenSSL docs: + # If the underlying BIO is non-blocking, SSL_read() will also return when the + # underlying BIO could not satisfy the needs of SSL_read() to continue the + # operation. In this case a call to SSL_get_error with the return value of + # SSL_read() will yield SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE. + if (time.time() - start) < self.o.gettimeout(): + time.sleep(0.1) + continue + else: + raise TcpTimeout() + except socket.timeout: + raise TcpTimeout() + except socket.error as e: + raise TcpDisconnect(str(e)) + except SSL.SysCallError as e: + if e.args == (-1, 'Unexpected EOF'): + break + raise TlsException(str(e)) + except SSL.Error as e: + raise TlsException(str(e)) + self.first_byte_timestamp = self.first_byte_timestamp or time.time() + if not data: + break + result += data + if length != -1: + length -= len(data) + self.add_log(result) + return result + + def readline(self, size=None): + result = b'' + bytes_read = 0 + while True: + if size is not None and bytes_read >= size: + break + ch = self.read(1) + bytes_read += 1 + if not ch: + break + else: + result += ch + if ch == b'\n': + break + return result + + def safe_read(self, length): + """ + Like .read, but is guaranteed to either return length bytes, or + raise an exception. + """ + result = self.read(length) + if length != -1 and len(result) != length: + if not result: + raise TcpDisconnect() + else: + raise TcpReadIncomplete( + "Expected %s bytes, got %s" % (length, len(result)) + ) + return result + + def peek(self, length): + """ + Tries to peek into the underlying file object. + + Returns: + Up to the next N bytes if peeking is successful. + + Raises: + TcpException if there was an error with the socket + TlsException if there was an error with pyOpenSSL. + NotImplementedError if the underlying file object is not a [pyOpenSSL] socket + """ + if isinstance(self.o, socket_fileobject): + try: + return self.o._sock.recv(length, socket.MSG_PEEK) + except socket.error as e: + raise TcpException(repr(e)) + elif isinstance(self.o, SSL.Connection): + try: + if tuple(int(x) for x in OpenSSL.__version__.split(".")[:2]) > (0, 15): + return self.o.recv(length, socket.MSG_PEEK) + else: + # TODO: remove once a new version is released + # Polyfill for pyOpenSSL <= 0.15.1 + # Taken from https://github.com/pyca/pyopenssl/commit/1d95dea7fea03c7c0df345a5ea30c12d8a0378d2 + buf = SSL._ffi.new("char[]", length) + result = SSL._lib.SSL_peek(self.o._ssl, buf, length) + self.o._raise_ssl_error(self.o._ssl, result) + return SSL._ffi.buffer(buf, result)[:] + except SSL.Error as e: + six.reraise(TlsException, TlsException(str(e)), sys.exc_info()[2]) + else: + raise NotImplementedError("Can only peek into (pyOpenSSL) sockets") + + +class Address(utils.Serializable): + + """ + This class wraps an IPv4/IPv6 tuple to provide named attributes and + ipv6 information. + """ + + def __init__(self, address, use_ipv6=False): + self.address = tuple(address) + self.use_ipv6 = use_ipv6 + + def get_state(self): + return { + "address": self.address, + "use_ipv6": self.use_ipv6 + } + + def set_state(self, state): + self.address = state["address"] + self.use_ipv6 = state["use_ipv6"] + + @classmethod + def from_state(cls, state): + return Address(**state) + + @classmethod + def wrap(cls, t): + if isinstance(t, cls): + return t + else: + return cls(t) + + def __call__(self): + return self.address + + @property + def host(self): + return self.address[0] + + @property + def port(self): + return self.address[1] + + @property + def use_ipv6(self): + return self.family == socket.AF_INET6 + + @use_ipv6.setter + def use_ipv6(self, b): + self.family = socket.AF_INET6 if b else socket.AF_INET + + def __repr__(self): + return "{}:{}".format(self.host, self.port) + + def __str__(self): + return str(self.address) + + def __eq__(self, other): + if not other: + return False + other = Address.wrap(other) + return (self.address, self.family) == (other.address, other.family) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.address) ^ 42 # different hash than the tuple alone. + + +def ssl_read_select(rlist, timeout): + """ + This is a wrapper around select.select() which also works for SSL.Connections + by taking ssl_connection.pending() into account. + + Caveats: + If .pending() > 0 for any of the connections in rlist, we avoid the select syscall + and **will not include any other connections which may or may not be ready**. + + Args: + rlist: wait until ready for reading + + Returns: + subset of rlist which is ready for reading. + """ + return [ + conn for conn in rlist + if isinstance(conn, SSL.Connection) and conn.pending() > 0 + ] or select.select(rlist, (), (), timeout)[0] + + +def close_socket(sock): + """ + Does a hard close of a socket, without emitting a RST. + """ + try: + # We already indicate that we close our end. + # may raise "Transport endpoint is not connected" on Linux + sock.shutdown(socket.SHUT_WR) + + # Section 4.2.2.13 of RFC 1122 tells us that a close() with any pending + # readable data could lead to an immediate RST being sent (which is the + # case on Windows). + # http://ia600609.us.archive.org/22/items/TheUltimateSo_lingerPageOrWhyIsMyTcpNotReliable/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable.html + # + # This in turn results in the following issue: If we send an error page + # to the client and then close the socket, the RST may be received by + # the client before the error page and the users sees a connection + # error rather than the error page. Thus, we try to empty the read + # buffer on Windows first. (see + # https://github.com/mitmproxy/mitmproxy/issues/527#issuecomment-93782988) + # + + if os.name == "nt": # pragma: no cover + # We cannot rely on the shutdown()-followed-by-read()-eof technique + # proposed by the page above: Some remote machines just don't send + # a TCP FIN, which would leave us in the unfortunate situation that + # recv() would block infinitely. As a workaround, we set a timeout + # here even if we are in blocking mode. + sock.settimeout(sock.gettimeout() or 20) + + # limit at a megabyte so that we don't read infinitely + for _ in range(1024 ** 3 // 4096): + # may raise a timeout/disconnect exception. + if not sock.recv(4096): + break + + # Now we can close the other half as well. + sock.shutdown(socket.SHUT_RD) + + except socket.error: + pass + + sock.close() + + +class _Connection(object): + + rbufsize = -1 + wbufsize = -1 + + def _makefile(self): + """ + Set up .rfile and .wfile attributes from .connection + """ + # Ideally, we would use the Buffered IO in Python 3 by default. + # Unfortunately, the implementation of .peek() is broken for n>1 bytes, + # as it may just return what's left in the buffer and not all the bytes we want. + # As a workaround, we just use unbuffered sockets directly. + # https://mail.python.org/pipermail/python-dev/2009-June/089986.html + if six.PY2: + self.rfile = Reader(self.connection.makefile('rb', self.rbufsize)) + self.wfile = Writer(self.connection.makefile('wb', self.wbufsize)) + else: + self.rfile = Reader(socket.SocketIO(self.connection, "rb")) + self.wfile = Writer(socket.SocketIO(self.connection, "wb")) + + def __init__(self, connection): + if connection: + self.connection = connection + self._makefile() + else: + self.connection = None + self.rfile = None + self.wfile = None + + self.ssl_established = False + self.finished = False + + def get_current_cipher(self): + if not self.ssl_established: + return None + + name = self.connection.get_cipher_name() + bits = self.connection.get_cipher_bits() + version = self.connection.get_cipher_version() + return name, bits, version + + def finish(self): + self.finished = True + # If we have an SSL connection, wfile.close == connection.close + # (We call _FileLike.set_descriptor(conn)) + # Closing the socket is not our task, therefore we don't call close + # then. + if not isinstance(self.connection, SSL.Connection): + if not getattr(self.wfile, "closed", False): + try: + self.wfile.flush() + self.wfile.close() + except TcpDisconnect: + pass + + self.rfile.close() + else: + try: + self.connection.shutdown() + except SSL.Error: + pass + + def _create_ssl_context(self, + method=SSL_DEFAULT_METHOD, + options=SSL_DEFAULT_OPTIONS, + verify_options=SSL.VERIFY_NONE, + ca_path=None, + ca_pemfile=None, + cipher_list=None, + alpn_protos=None, + alpn_select=None, + alpn_select_callback=None, + ): + """ + Creates an SSL Context. + + :param method: One of SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD, TLSv1_1_METHOD, or TLSv1_2_METHOD + :param options: A bit field consisting of OpenSSL.SSL.OP_* values + :param verify_options: A bit field consisting of OpenSSL.SSL.VERIFY_* values + :param ca_path: Path to a directory of trusted CA certificates prepared using the c_rehash tool + :param ca_pemfile: Path to a PEM formatted trusted CA certificate + :param cipher_list: A textual OpenSSL cipher list, see https://www.openssl.org/docs/apps/ciphers.html + :rtype : SSL.Context + """ + context = SSL.Context(method) + # Options (NO_SSLv2/3) + if options is not None: + context.set_options(options) + + # Verify Options (NONE/PEER and trusted CAs) + if verify_options is not None: + def verify_cert(conn, x509, errno, err_depth, is_cert_verified): + if not is_cert_verified: + self.ssl_verification_error = dict(errno=errno, + depth=err_depth) + return is_cert_verified + + context.set_verify(verify_options, verify_cert) + if ca_path is None and ca_pemfile is None: + ca_pemfile = certifi.where() + context.load_verify_locations(ca_pemfile, ca_path) + + # Workaround for + # https://github.com/pyca/pyopenssl/issues/190 + # https://github.com/mitmproxy/mitmproxy/issues/472 + # Options already set before are not cleared. + context.set_mode(SSL._lib.SSL_MODE_AUTO_RETRY) + + # Cipher List + if cipher_list: + try: + context.set_cipher_list(cipher_list) + + # TODO: maybe change this to with newer pyOpenSSL APIs + context.set_tmp_ecdh(OpenSSL.crypto.get_elliptic_curve('prime256v1')) + except SSL.Error as v: + raise TlsException("SSL cipher specification error: %s" % str(v)) + + # SSLKEYLOGFILE + if log_ssl_key: + context.set_info_callback(log_ssl_key) + + if HAS_ALPN: + if alpn_protos is not None: + # advertise application layer protocols + context.set_alpn_protos(alpn_protos) + elif alpn_select is not None and alpn_select_callback is None: + # select application layer protocol + def alpn_select_callback(conn_, options): + if alpn_select in options: + return bytes(alpn_select) + else: # pragma no cover + return options[0] + context.set_alpn_select_callback(alpn_select_callback) + elif alpn_select_callback is not None and alpn_select is None: + context.set_alpn_select_callback(alpn_select_callback) + elif alpn_select_callback is not None and alpn_select is not None: + raise TlsException("ALPN error: only define alpn_select (string) OR alpn_select_callback (method).") + + return context + + +class TCPClient(_Connection): + + def __init__(self, address, source_address=None): + super(TCPClient, self).__init__(None) + self.address = address + self.source_address = source_address + self.cert = None + self.ssl_verification_error = None + self.sni = None + + @property + def address(self): + return self.__address + + @address.setter + def address(self, address): + if address: + self.__address = Address.wrap(address) + else: + self.__address = None + + @property + def source_address(self): + return self.__source_address + + @source_address.setter + def source_address(self, source_address): + if source_address: + self.__source_address = Address.wrap(source_address) + else: + self.__source_address = None + + def close(self): + # Make sure to close the real socket, not the SSL proxy. + # OpenSSL is really good at screwing up, i.e. when trying to recv from a failed connection, + # it tries to renegotiate... + if isinstance(self.connection, SSL.Connection): + close_socket(self.connection._socket) + else: + close_socket(self.connection) + + def create_ssl_context(self, cert=None, alpn_protos=None, **sslctx_kwargs): + context = self._create_ssl_context( + alpn_protos=alpn_protos, + **sslctx_kwargs) + # Client Certs + if cert: + try: + context.use_privatekey_file(cert) + context.use_certificate_file(cert) + except SSL.Error as v: + raise TlsException("SSL client certificate error: %s" % str(v)) + return context + + def convert_to_ssl(self, sni=None, alpn_protos=None, **sslctx_kwargs): + """ + cert: Path to a file containing both client cert and private key. + + options: A bit field consisting of OpenSSL.SSL.OP_* values + verify_options: A bit field consisting of OpenSSL.SSL.VERIFY_* values + ca_path: Path to a directory of trusted CA certificates prepared using the c_rehash tool + ca_pemfile: Path to a PEM formatted trusted CA certificate + """ + verification_mode = sslctx_kwargs.get('verify_options', None) + if verification_mode == SSL.VERIFY_PEER and not sni: + raise TlsException("Cannot validate certificate hostname without SNI") + + context = self.create_ssl_context( + alpn_protos=alpn_protos, + **sslctx_kwargs + ) + self.connection = SSL.Connection(context, self.connection) + if sni: + self.sni = sni + self.connection.set_tlsext_host_name(sni) + self.connection.set_connect_state() + try: + self.connection.do_handshake() + except SSL.Error as v: + if self.ssl_verification_error: + raise InvalidCertificateException("SSL handshake error: %s" % repr(v)) + else: + raise TlsException("SSL handshake error: %s" % repr(v)) + else: + # Fix for pre v1.0 OpenSSL, which doesn't throw an exception on + # certificate validation failure + if verification_mode == SSL.VERIFY_PEER and self.ssl_verification_error is not None: + raise InvalidCertificateException("SSL handshake error: certificate verify failed") + + self.cert = certutils.SSLCert(self.connection.get_peer_certificate()) + + # Validate TLS Hostname + try: + crt = dict( + subjectAltName=[("DNS", x.decode("ascii", "strict")) for x in self.cert.altnames] + ) + if self.cert.cn: + crt["subject"] = [[["commonName", self.cert.cn.decode("ascii", "strict")]]] + if sni: + hostname = sni.decode("ascii", "strict") + else: + hostname = "no-hostname" + ssl_match_hostname.match_hostname(crt, hostname) + except (ValueError, ssl_match_hostname.CertificateError) as e: + self.ssl_verification_error = dict(depth=0, errno="Invalid Hostname") + if verification_mode == SSL.VERIFY_PEER: + raise InvalidCertificateException("Presented certificate for {} is not valid: {}".format(sni, str(e))) + + self.ssl_established = True + self.rfile.set_descriptor(self.connection) + self.wfile.set_descriptor(self.connection) + + def connect(self): + try: + connection = socket.socket(self.address.family, socket.SOCK_STREAM) + if self.source_address: + connection.bind(self.source_address()) + connection.connect(self.address()) + if not self.source_address: + self.source_address = Address(connection.getsockname()) + except (socket.error, IOError) as err: + raise TcpException( + 'Error connecting to "%s": %s' % + (self.address.host, err)) + self.connection = connection + self._makefile() + + def settimeout(self, n): + self.connection.settimeout(n) + + def gettimeout(self): + return self.connection.gettimeout() + + def get_alpn_proto_negotiated(self): + if HAS_ALPN and self.ssl_established: + return self.connection.get_alpn_proto_negotiated() + else: + return b"" + + +class BaseHandler(_Connection): + + """ + The instantiator is expected to call the handle() and finish() methods. + """ + + def __init__(self, connection, address, server): + super(BaseHandler, self).__init__(connection) + self.address = Address.wrap(address) + self.server = server + self.clientcert = None + + def create_ssl_context(self, + cert, key, + handle_sni=None, + request_client_cert=None, + chain_file=None, + dhparams=None, + **sslctx_kwargs): + """ + cert: A certutils.SSLCert object or the path to a certificate + chain file. + + handle_sni: SNI handler, should take a connection object. Server + name can be retrieved like this: + + connection.get_servername() + + And you can specify the connection keys as follows: + + new_context = Context(TLSv1_METHOD) + new_context.use_privatekey(key) + new_context.use_certificate(cert) + connection.set_context(new_context) + + The request_client_cert argument requires some explanation. We're + supposed to be able to do this with no negative effects - if the + client has no cert to present, we're notified and proceed as usual. + Unfortunately, Android seems to have a bug (tested on 4.2.2) - when + an Android client is asked to present a certificate it does not + have, it hangs up, which is frankly bogus. Some time down the track + we may be able to make the proper behaviour the default again, but + until then we're conservative. + """ + + context = self._create_ssl_context(**sslctx_kwargs) + + context.use_privatekey(key) + if isinstance(cert, certutils.SSLCert): + context.use_certificate(cert.x509) + else: + context.use_certificate_chain_file(cert) + + if handle_sni: + # SNI callback happens during do_handshake() + context.set_tlsext_servername_callback(handle_sni) + + if request_client_cert: + def save_cert(conn_, cert, errno_, depth_, preverify_ok_): + self.clientcert = certutils.SSLCert(cert) + # Return true to prevent cert verification error + return True + context.set_verify(SSL.VERIFY_PEER, save_cert) + + # Cert Verify + if chain_file: + context.load_verify_locations(chain_file) + + if dhparams: + SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) + + return context + + def convert_to_ssl(self, cert, key, **sslctx_kwargs): + """ + Convert connection to SSL. + For a list of parameters, see BaseHandler._create_ssl_context(...) + """ + + context = self.create_ssl_context( + cert, + key, + **sslctx_kwargs) + self.connection = SSL.Connection(context, self.connection) + self.connection.set_accept_state() + try: + self.connection.do_handshake() + except SSL.Error as v: + raise TlsException("SSL handshake error: %s" % repr(v)) + self.ssl_established = True + self.rfile.set_descriptor(self.connection) + self.wfile.set_descriptor(self.connection) + + def handle(self): # pragma: no cover + raise NotImplementedError + + def settimeout(self, n): + self.connection.settimeout(n) + + def get_alpn_proto_negotiated(self): + if HAS_ALPN and self.ssl_established: + return self.connection.get_alpn_proto_negotiated() + else: + return b"" + + +class TCPServer(object): + request_queue_size = 20 + + def __init__(self, address): + self.address = Address.wrap(address) + self.__is_shut_down = threading.Event() + self.__shutdown_request = False + self.socket = socket.socket(self.address.family, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind(self.address()) + self.address = Address.wrap(self.socket.getsockname()) + self.socket.listen(self.request_queue_size) + + def connection_thread(self, connection, client_address): + client_address = Address(client_address) + try: + self.handle_client_connection(connection, client_address) + except: + self.handle_error(connection, client_address) + finally: + close_socket(connection) + + def serve_forever(self, poll_interval=0.1): + self.__is_shut_down.clear() + try: + while not self.__shutdown_request: + try: + r, w_, e_ = select.select( + [self.socket], [], [], poll_interval) + except select.error as ex: # pragma: no cover + if ex[0] == EINTR: + continue + else: + raise + if self.socket in r: + connection, client_address = self.socket.accept() + t = threading.Thread( + target=self.connection_thread, + args=(connection, client_address), + name="ConnectionThread (%s:%s -> %s:%s)" % + (client_address[0], client_address[1], + self.address.host, self.address.port) + ) + t.setDaemon(1) + try: + t.start() + except threading.ThreadError: + self.handle_error(connection, Address(client_address)) + connection.close() + finally: + self.__shutdown_request = False + self.__is_shut_down.set() + + def shutdown(self): + self.__shutdown_request = True + self.__is_shut_down.wait() + self.socket.close() + self.handle_shutdown() + + def handle_error(self, connection_, client_address, fp=sys.stderr): + """ + Called when handle_client_connection raises an exception. + """ + # If a thread has persisted after interpreter exit, the module might be + # none. + if traceback: + exc = six.text_type(traceback.format_exc()) + print(u'-' * 40, file=fp) + print( + u"Error in processing of request from %s" % repr(client_address), file=fp) + print(exc, file=fp) + print(u'-' * 40, file=fp) + + def handle_client_connection(self, conn, client_address): # pragma: no cover + """ + Called after client connection. + """ + raise NotImplementedError + + def handle_shutdown(self): + """ + Called after server shutdown. + """ diff --git a/netlib/tutils.py b/netlib/tutils.py new file mode 100644 index 00000000..f6ce8e0a --- /dev/null +++ b/netlib/tutils.py @@ -0,0 +1,133 @@ +from io import BytesIO +import tempfile +import os +import time +import shutil +from contextlib import contextmanager +import six +import sys + +from . import utils, tcp +from .http import Request, Response, Headers + + +def treader(bytes): + """ + Construct a tcp.Read object from bytes. + """ + fp = BytesIO(bytes) + return tcp.Reader(fp) + + +@contextmanager +def tmpdir(*args, **kwargs): + orig_workdir = os.getcwd() + temp_workdir = tempfile.mkdtemp(*args, **kwargs) + os.chdir(temp_workdir) + + yield temp_workdir + + os.chdir(orig_workdir) + shutil.rmtree(temp_workdir) + + +def _check_exception(expected, actual, exc_tb): + if isinstance(expected, six.string_types): + if expected.lower() not in str(actual).lower(): + six.reraise(AssertionError, AssertionError( + "Expected %s, but caught %s" % ( + repr(expected), repr(actual) + ) + ), exc_tb) + else: + if not isinstance(actual, expected): + six.reraise(AssertionError, AssertionError( + "Expected %s, but caught %s %s" % ( + expected.__name__, actual.__class__.__name__, repr(actual) + ) + ), exc_tb) + + +def raises(expected_exception, obj=None, *args, **kwargs): + """ + Assert that a callable raises a specified exception. + + :exc An exception class or a string. If a class, assert that an + exception of this type is raised. If a string, assert that the string + occurs in the string representation of the exception, based on a + case-insenstivie match. + + :obj A callable object. + + :args Arguments to be passsed to the callable. + + :kwargs Arguments to be passed to the callable. + """ + if obj is None: + return RaisesContext(expected_exception) + else: + try: + ret = obj(*args, **kwargs) + except Exception as actual: + _check_exception(expected_exception, actual, sys.exc_info()[2]) + else: + raise AssertionError("No exception raised. Return value: {}".format(ret)) + + +class RaisesContext(object): + def __init__(self, expected_exception): + self.expected_exception = expected_exception + + def __enter__(self): + return + + def __exit__(self, exc_type, exc_val, exc_tb): + if not exc_type: + raise AssertionError("No exception raised.") + else: + _check_exception(self.expected_exception, exc_val, exc_tb) + return True + + +test_data = utils.Data(__name__) +# FIXME: Temporary workaround during repo merge. +import os +test_data.dirname = os.path.join(test_data.dirname,"..","..","test","netlib") + + +def treq(**kwargs): + """ + Returns: + netlib.http.Request + """ + default = dict( + first_line_format="relative", + method=b"GET", + scheme=b"http", + host=b"address", + port=22, + path=b"/path", + http_version=b"HTTP/1.1", + headers=Headers(header="qvalue", content_length="7"), + content=b"content" + ) + default.update(kwargs) + return Request(**default) + + +def tresp(**kwargs): + """ + Returns: + netlib.http.Response + """ + default = dict( + http_version=b"HTTP/1.1", + status_code=200, + reason=b"OK", + headers=Headers(header_response="svalue", content_length="7"), + content=b"message", + timestamp_start=time.time(), + timestamp_end=time.time(), + ) + default.update(kwargs) + return Response(**default) diff --git a/netlib/utils.py b/netlib/utils.py new file mode 100644 index 00000000..f7bb5c4b --- /dev/null +++ b/netlib/utils.py @@ -0,0 +1,418 @@ +from __future__ import absolute_import, print_function, division +import os.path +import re +import codecs +import unicodedata +from abc import ABCMeta, abstractmethod +import importlib +import inspect + +import six + +from six.moves import urllib +import hyperframe + + +@six.add_metaclass(ABCMeta) +class Serializable(object): + """ + Abstract Base Class that defines an API to save an object's state and restore it later on. + """ + + @classmethod + @abstractmethod + def from_state(cls, state): + """ + Create a new object from the given state. + """ + raise NotImplementedError() + + @abstractmethod + def get_state(self): + """ + Retrieve object state. + """ + raise NotImplementedError() + + @abstractmethod + def set_state(self, state): + """ + Set object state to the given state. + """ + raise NotImplementedError() + + +def always_bytes(unicode_or_bytes, *encode_args): + if isinstance(unicode_or_bytes, six.text_type): + return unicode_or_bytes.encode(*encode_args) + return unicode_or_bytes + + +def always_byte_args(*encode_args): + """Decorator that transparently encodes all arguments passed as unicode""" + def decorator(fun): + def _fun(*args, **kwargs): + args = [always_bytes(arg, *encode_args) for arg in args] + kwargs = {k: always_bytes(v, *encode_args) for k, v in six.iteritems(kwargs)} + return fun(*args, **kwargs) + return _fun + return decorator + + +def native(s, *encoding_opts): + """ + Convert :py:class:`bytes` or :py:class:`unicode` to the native + :py:class:`str` type, using latin1 encoding if conversion is necessary. + + https://www.python.org/dev/peps/pep-3333/#a-note-on-string-types + """ + if not isinstance(s, (six.binary_type, six.text_type)): + raise TypeError("%r is neither bytes nor unicode" % s) + if six.PY3: + if isinstance(s, six.binary_type): + return s.decode(*encoding_opts) + else: + if isinstance(s, six.text_type): + return s.encode(*encoding_opts) + return s + + +def isascii(bytes): + try: + bytes.decode("ascii") + except ValueError: + return False + return True + + +def clean_bin(s, keep_spacing=True): + """ + Cleans binary data to make it safe to display. + + Args: + keep_spacing: If False, tabs and newlines will also be replaced. + """ + if isinstance(s, six.text_type): + if keep_spacing: + keep = u" \n\r\t" + else: + keep = u" " + return u"".join( + ch if (unicodedata.category(ch)[0] not in "CZ" or ch in keep) else u"." + for ch in s + ) + else: + if keep_spacing: + keep = (9, 10, 13) # \t, \n, \r, + else: + keep = () + return b"".join( + six.int2byte(ch) if (31 < ch < 127 or ch in keep) else b"." + for ch in six.iterbytes(s) + ) + + +def hexdump(s): + """ + Returns: + A generator of (offset, hex, str) tuples + """ + for i in range(0, len(s), 16): + offset = "{:0=10x}".format(i).encode() + part = s[i:i + 16] + x = b" ".join("{:0=2x}".format(i).encode() for i in six.iterbytes(part)) + x = x.ljust(47) # 16*2 + 15 + yield (offset, x, clean_bin(part, False)) + + +def setbit(byte, offset, value): + """ + Set a bit in a byte to 1 if value is truthy, 0 if not. + """ + if value: + return byte | (1 << offset) + else: + return byte & ~(1 << offset) + + +def getbit(byte, offset): + mask = 1 << offset + return bool(byte & mask) + + +class BiDi(object): + + """ + A wee utility class for keeping bi-directional mappings, like field + constants in protocols. Names are attributes on the object, dict-like + access maps values to names: + + CONST = BiDi(a=1, b=2) + assert CONST.a == 1 + assert CONST.get_name(1) == "a" + """ + + def __init__(self, **kwargs): + self.names = kwargs + self.values = {} + for k, v in kwargs.items(): + self.values[v] = k + if len(self.names) != len(self.values): + raise ValueError("Duplicate values not allowed.") + + def __getattr__(self, k): + if k in self.names: + return self.names[k] + raise AttributeError("No such attribute: %s", k) + + def get_name(self, n, default=None): + return self.values.get(n, default) + + +def pretty_size(size): + suffixes = [ + ("B", 2 ** 10), + ("kB", 2 ** 20), + ("MB", 2 ** 30), + ] + for suf, lim in suffixes: + if size >= lim: + continue + else: + x = round(size / float(lim / 2 ** 10), 2) + if x == int(x): + x = int(x) + return str(x) + suf + + +class Data(object): + + def __init__(self, name): + m = importlib.import_module(name) + dirname = os.path.dirname(inspect.getsourcefile(m)) + self.dirname = os.path.abspath(dirname) + + def path(self, path): + """ + Returns a path to the package data housed at 'path' under this + module.Path can be a path to a file, or to a directory. + + This function will raise ValueError if the path does not exist. + """ + fullpath = os.path.join(self.dirname, path) + if not os.path.exists(fullpath): + raise ValueError("dataPath: %s does not exist." % fullpath) + return fullpath + + +_label_valid = re.compile(b"(?!-)[A-Z\d-]{1,63}(? 255: + return False + if host[-1] == b".": + host = host[:-1] + return all(_label_valid.match(x) for x in host.split(b".")) + + +def is_valid_port(port): + return 0 <= port <= 65535 + + +# PY2 workaround +def decode_parse_result(result, enc): + if hasattr(result, "decode"): + return result.decode(enc) + else: + return urllib.parse.ParseResult(*[x.decode(enc) for x in result]) + + +# PY2 workaround +def encode_parse_result(result, enc): + if hasattr(result, "encode"): + return result.encode(enc) + else: + return urllib.parse.ParseResult(*[x.encode(enc) for x in result]) + + +def parse_url(url): + """ + URL-parsing function that checks that + - port is an integer 0-65535 + - host is a valid IDNA-encoded hostname with no null-bytes + - path is valid ASCII + + Args: + A URL (as bytes or as unicode) + + Returns: + A (scheme, host, port, path) tuple + + Raises: + ValueError, if the URL is not properly formatted. + """ + parsed = urllib.parse.urlparse(url) + + if not parsed.hostname: + raise ValueError("No hostname given") + + if isinstance(url, six.binary_type): + host = parsed.hostname + + # this should not raise a ValueError, + # but we try to be very forgiving here and accept just everything. + # decode_parse_result(parsed, "ascii") + else: + host = parsed.hostname.encode("idna") + parsed = encode_parse_result(parsed, "ascii") + + port = parsed.port + if not port: + port = 443 if parsed.scheme == b"https" else 80 + + full_path = urllib.parse.urlunparse( + (b"", b"", parsed.path, parsed.params, parsed.query, parsed.fragment) + ) + if not full_path.startswith(b"/"): + full_path = b"/" + full_path + + if not is_valid_host(host): + raise ValueError("Invalid Host") + if not is_valid_port(port): + raise ValueError("Invalid Port") + + return parsed.scheme, host, port, full_path + + +def get_header_tokens(headers, key): + """ + Retrieve all tokens for a header key. A number of different headers + follow a pattern where each header line can containe comma-separated + tokens, and headers can be set multiple times. + """ + if key not in headers: + return [] + tokens = headers[key].split(",") + return [token.strip() for token in tokens] + + +def hostport(scheme, host, port): + """ + Returns the host component, with a port specifcation if needed. + """ + if (port, scheme) in [(80, "http"), (443, "https"), (80, b"http"), (443, b"https")]: + return host + else: + if isinstance(host, six.binary_type): + return b"%s:%d" % (host, port) + else: + return "%s:%d" % (host, port) + + +def unparse_url(scheme, host, port, path=""): + """ + Returns a URL string, constructed from the specified components. + + Args: + All args must be str. + """ + return "%s://%s%s" % (scheme, hostport(scheme, host, port), path) + + +def urlencode(s): + """ + Takes a list of (key, value) tuples and returns a urlencoded string. + """ + s = [tuple(i) for i in s] + return urllib.parse.urlencode(s, False) + + +def urldecode(s): + """ + Takes a urlencoded string and returns a list of (key, value) tuples. + """ + return urllib.parse.parse_qsl(s, keep_blank_values=True) + + +def parse_content_type(c): + """ + A simple parser for content-type values. Returns a (type, subtype, + parameters) tuple, where type and subtype are strings, and parameters + is a dict. If the string could not be parsed, return None. + + E.g. the following string: + + text/html; charset=UTF-8 + + Returns: + + ("text", "html", {"charset": "UTF-8"}) + """ + parts = c.split(";", 1) + ts = parts[0].split("/", 1) + if len(ts) != 2: + return None + d = {} + if len(parts) == 2: + for i in parts[1].split(";"): + clause = i.split("=", 1) + if len(clause) == 2: + d[clause[0].strip()] = clause[1].strip() + return ts[0].lower(), ts[1].lower(), d + + +def multipartdecode(headers, content): + """ + Takes a multipart boundary encoded string and returns list of (key, value) tuples. + """ + v = headers.get("content-type") + if v: + v = parse_content_type(v) + if not v: + return [] + try: + boundary = v[2]["boundary"].encode("ascii") + except (KeyError, UnicodeError): + return [] + + rx = re.compile(br'\bname="([^"]+)"') + r = [] + + for i in content.split(b"--" + boundary): + parts = i.splitlines() + if len(parts) > 1 and parts[0][0:2] != b"--": + match = rx.search(parts[1]) + if match: + key = match.group(1) + value = b"".join(parts[3 + parts[2:].index(b""):]) + r.append((key, value)) + return r + return [] + + +def http2_read_raw_frame(rfile): + header = rfile.safe_read(9) + length = int(codecs.encode(header[:3], 'hex_codec'), 16) + + if length == 4740180: + raise ValueError("Length field looks more like HTTP/1.1: %s" % rfile.peek(20)) + + body = rfile.safe_read(length) + return [header, body] + +def http2_read_frame(rfile): + header, body = http2_read_raw_frame(rfile) + frame, length = hyperframe.frame.Frame.parse_frame_header(header) + frame.parse_body(memoryview(body)) + return frame diff --git a/netlib/version.py b/netlib/version.py new file mode 100644 index 00000000..379fee0f --- /dev/null +++ b/netlib/version.py @@ -0,0 +1,6 @@ +from __future__ import (absolute_import, print_function, division) + +IVERSION = (0, 17) +VERSION = ".".join(str(i) for i in IVERSION) +NAME = "netlib" +NAMEVERSION = NAME + " " + VERSION diff --git a/netlib/version_check.py b/netlib/version_check.py new file mode 100644 index 00000000..9cf27eea --- /dev/null +++ b/netlib/version_check.py @@ -0,0 +1,60 @@ +""" +Having installed a wrong version of pyOpenSSL or netlib is unfortunately a +very common source of error. Check before every start that both versions +are somewhat okay. +""" +from __future__ import division, absolute_import, print_function +import sys +import inspect +import os.path +import six + +import OpenSSL +from . import version + +PYOPENSSL_MIN_VERSION = (0, 15) + + +def check_mitmproxy_version(mitmproxy_version, fp=sys.stderr): + # We don't introduce backward-incompatible changes in patch versions. Only + # consider major and minor version. + if version.IVERSION[:2] != mitmproxy_version[:2]: + print( + u"You are using mitmproxy %s with netlib %s. " + u"Most likely, that won't work - please upgrade!" % ( + mitmproxy_version, version.VERSION + ), + file=fp + ) + sys.exit(1) + + +def check_pyopenssl_version(min_version=PYOPENSSL_MIN_VERSION, fp=sys.stderr): + min_version_str = u".".join(six.text_type(x) for x in min_version) + try: + v = tuple(int(x) for x in OpenSSL.__version__.split(".")[:2]) + except ValueError: + print( + u"Cannot parse pyOpenSSL version: {}" + u"mitmproxy requires pyOpenSSL {} or greater.".format( + OpenSSL.__version__, min_version_str + ), + file=fp + ) + return + if v < min_version: + print( + u"You are using an outdated version of pyOpenSSL: " + u"mitmproxy requires pyOpenSSL {} or greater.".format(min_version_str), + file=fp + ) + # Some users apparently have multiple versions of pyOpenSSL installed. + # Report which one we got. + pyopenssl_path = os.path.dirname(inspect.getfile(OpenSSL)) + print( + u"Your pyOpenSSL {} installation is located at {}".format( + OpenSSL.__version__, pyopenssl_path + ), + file=fp + ) + sys.exit(1) diff --git a/netlib/websockets/__init__.py b/netlib/websockets/__init__.py new file mode 100644 index 00000000..1c143919 --- /dev/null +++ b/netlib/websockets/__init__.py @@ -0,0 +1,2 @@ +from .frame import * +from .protocol import * diff --git a/netlib/websockets/frame.py b/netlib/websockets/frame.py new file mode 100644 index 00000000..fce2c9d3 --- /dev/null +++ b/netlib/websockets/frame.py @@ -0,0 +1,316 @@ +from __future__ import absolute_import +import os +import struct +import io +import warnings + +import six + +from .protocol import Masker +from netlib import tcp +from netlib import utils + + +MAX_16_BIT_INT = (1 << 16) +MAX_64_BIT_INT = (1 << 64) + +DEFAULT=object() + +OPCODE = utils.BiDi( + CONTINUE=0x00, + TEXT=0x01, + BINARY=0x02, + CLOSE=0x08, + PING=0x09, + PONG=0x0a +) + + +class FrameHeader(object): + + def __init__( + self, + opcode=OPCODE.TEXT, + payload_length=0, + fin=False, + rsv1=False, + rsv2=False, + rsv3=False, + masking_key=DEFAULT, + mask=DEFAULT, + length_code=DEFAULT + ): + if not 0 <= opcode < 2 ** 4: + raise ValueError("opcode must be 0-16") + self.opcode = opcode + self.payload_length = payload_length + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + + if length_code is DEFAULT: + self.length_code = self._make_length_code(self.payload_length) + else: + self.length_code = length_code + + if mask is DEFAULT and masking_key is DEFAULT: + self.mask = False + self.masking_key = b"" + elif mask is DEFAULT: + self.mask = 1 + self.masking_key = masking_key + elif masking_key is DEFAULT: + self.mask = mask + self.masking_key = os.urandom(4) + else: + self.mask = mask + self.masking_key = masking_key + + if self.masking_key and len(self.masking_key) != 4: + raise ValueError("Masking key must be 4 bytes.") + + @classmethod + def _make_length_code(self, length): + """ + A websockets frame contains an initial length_code, and an optional + extended length code to represent the actual length if length code is + larger than 125 + """ + if length <= 125: + return length + elif length >= 126 and length <= 65535: + return 126 + else: + return 127 + + def __repr__(self): + vals = [ + "ws frame:", + OPCODE.get_name(self.opcode, hex(self.opcode)).lower() + ] + flags = [] + for i in ["fin", "rsv1", "rsv2", "rsv3", "mask"]: + if getattr(self, i): + flags.append(i) + if flags: + vals.extend([":", "|".join(flags)]) + if self.masking_key: + vals.append(":key=%s" % repr(self.masking_key)) + if self.payload_length: + vals.append(" %s" % utils.pretty_size(self.payload_length)) + return "".join(vals) + + def human_readable(self): + warnings.warn("FrameHeader.to_bytes is deprecated, use bytes(frame_header) instead.", DeprecationWarning) + return repr(self) + + def __bytes__(self): + first_byte = utils.setbit(0, 7, self.fin) + first_byte = utils.setbit(first_byte, 6, self.rsv1) + first_byte = utils.setbit(first_byte, 5, self.rsv2) + first_byte = utils.setbit(first_byte, 4, self.rsv3) + first_byte = first_byte | self.opcode + + second_byte = utils.setbit(self.length_code, 7, self.mask) + + b = six.int2byte(first_byte) + six.int2byte(second_byte) + + if self.payload_length < 126: + pass + elif self.payload_length < MAX_16_BIT_INT: + # '!H' pack as 16 bit unsigned short + # add 2 byte extended payload length + b += struct.pack('!H', self.payload_length) + elif self.payload_length < MAX_64_BIT_INT: + # '!Q' = pack as 64 bit unsigned long long + # add 8 bytes extended payload length + b += struct.pack('!Q', self.payload_length) + if self.masking_key: + b += self.masking_key + return b + + if six.PY2: + __str__ = __bytes__ + + def to_bytes(self): + warnings.warn("FrameHeader.to_bytes is deprecated, use bytes(frame_header) instead.", DeprecationWarning) + return bytes(self) + + @classmethod + def from_file(cls, fp): + """ + read a websockets frame header + """ + first_byte = six.byte2int(fp.safe_read(1)) + second_byte = six.byte2int(fp.safe_read(1)) + + fin = utils.getbit(first_byte, 7) + rsv1 = utils.getbit(first_byte, 6) + rsv2 = utils.getbit(first_byte, 5) + rsv3 = utils.getbit(first_byte, 4) + # grab right-most 4 bits + opcode = first_byte & 15 + mask_bit = utils.getbit(second_byte, 7) + # grab the next 7 bits + length_code = second_byte & 127 + + # payload_lengthy > 125 indicates you need to read more bytes + # to get the actual payload length + if length_code <= 125: + payload_length = length_code + elif length_code == 126: + payload_length, = struct.unpack("!H", fp.safe_read(2)) + elif length_code == 127: + payload_length, = struct.unpack("!Q", fp.safe_read(8)) + + # masking key only present if mask bit set + if mask_bit == 1: + masking_key = fp.safe_read(4) + else: + masking_key = None + + return cls( + fin=fin, + rsv1=rsv1, + rsv2=rsv2, + rsv3=rsv3, + opcode=opcode, + mask=mask_bit, + length_code=length_code, + payload_length=payload_length, + masking_key=masking_key, + ) + + def __eq__(self, other): + if isinstance(other, FrameHeader): + return bytes(self) == bytes(other) + return False + + +class Frame(object): + + """ + Represents one websockets frame. + Constructor takes human readable forms of the frame components + from_bytes() is also avaliable. + + WebSockets Frame as defined in RFC6455 + + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ + """ + + def __init__(self, payload=b"", **kwargs): + self.payload = payload + kwargs["payload_length"] = kwargs.get("payload_length", len(payload)) + self.header = FrameHeader(**kwargs) + + @classmethod + def default(cls, message, from_client=False): + """ + Construct a basic websocket frame from some default values. + Creates a non-fragmented text frame. + """ + if from_client: + mask_bit = 1 + masking_key = os.urandom(4) + else: + mask_bit = 0 + masking_key = None + + return cls( + message, + fin=1, # final frame + opcode=OPCODE.TEXT, # text + mask=mask_bit, + masking_key=masking_key, + ) + + @classmethod + def from_bytes(cls, bytestring): + """ + Construct a websocket frame from an in-memory bytestring + to construct a frame from a stream of bytes, use from_file() directly + """ + return cls.from_file(tcp.Reader(io.BytesIO(bytestring))) + + def __repr__(self): + ret = repr(self.header) + if self.payload: + ret = ret + "\nPayload:\n" + utils.clean_bin(self.payload).decode("ascii") + return ret + + def human_readable(self): + warnings.warn("Frame.to_bytes is deprecated, use bytes(frame) instead.", DeprecationWarning) + return repr(self) + + def __bytes__(self): + """ + Serialize the frame to wire format. Returns a string. + """ + b = bytes(self.header) + if self.header.masking_key: + b += Masker(self.header.masking_key)(self.payload) + else: + b += self.payload + return b + + if six.PY2: + __str__ = __bytes__ + + def to_bytes(self): + warnings.warn("FrameHeader.to_bytes is deprecated, use bytes(frame_header) instead.", DeprecationWarning) + return bytes(self) + + def to_file(self, writer): + warnings.warn("Frame.to_file is deprecated, use wfile.write(bytes(frame)) instead.", DeprecationWarning) + writer.write(bytes(self)) + writer.flush() + + @classmethod + def from_file(cls, fp): + """ + read a websockets frame sent by a server or client + + fp is a "file like" object that could be backed by a network + stream or a disk or an in memory stream reader + """ + header = FrameHeader.from_file(fp) + payload = fp.safe_read(header.payload_length) + + if header.mask == 1 and header.masking_key: + payload = Masker(header.masking_key)(payload) + + return cls( + payload, + fin=header.fin, + opcode=header.opcode, + mask=header.mask, + payload_length=header.payload_length, + masking_key=header.masking_key, + rsv1=header.rsv1, + rsv2=header.rsv2, + rsv3=header.rsv3, + length_code=header.length_code + ) + + def __eq__(self, other): + if isinstance(other, Frame): + return bytes(self) == bytes(other) + return False diff --git a/netlib/websockets/protocol.py b/netlib/websockets/protocol.py new file mode 100644 index 00000000..1e95fa1c --- /dev/null +++ b/netlib/websockets/protocol.py @@ -0,0 +1,115 @@ + + + +# Colleciton of utility functions that implement small portions of the RFC6455 +# WebSockets Protocol Useful for building WebSocket clients and servers. +# +# Emphassis is on readabilty, simplicity and modularity, not performance or +# completeness +# +# This is a work in progress and does not yet contain all the utilites need to +# create fully complient client/servers # +# Spec: https://tools.ietf.org/html/rfc6455 + +# The magic sha that websocket servers must know to prove they understand +# RFC6455 +from __future__ import absolute_import +import base64 +import hashlib +import os + +import binascii +import six +from ..http import Headers + +websockets_magic = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' +VERSION = "13" + + +class Masker(object): + + """ + Data sent from the server must be masked to prevent malicious clients + from sending data over the wire in predictable patterns + + Servers do not have to mask data they send to the client. + https://tools.ietf.org/html/rfc6455#section-5.3 + """ + + def __init__(self, key): + self.key = key + self.offset = 0 + + def mask(self, offset, data): + result = bytearray(data) + if six.PY2: + for i in range(len(data)): + result[i] ^= ord(self.key[offset % 4]) + offset += 1 + result = str(result) + else: + + for i in range(len(data)): + result[i] ^= self.key[offset % 4] + offset += 1 + result = bytes(result) + return result + + def __call__(self, data): + ret = self.mask(self.offset, data) + self.offset += len(ret) + return ret + + +class WebsocketsProtocol(object): + + def __init__(self): + pass + + @classmethod + def client_handshake_headers(self, key=None, version=VERSION): + """ + Create the headers for a valid HTTP upgrade request. If Key is not + specified, it is generated, and can be found in sec-websocket-key in + the returned header set. + + Returns an instance of Headers + """ + if not key: + key = base64.b64encode(os.urandom(16)).decode('ascii') + return Headers( + sec_websocket_key=key, + sec_websocket_version=version, + connection="Upgrade", + upgrade="websocket", + ) + + @classmethod + def server_handshake_headers(self, key): + """ + The server response is a valid HTTP 101 response. + """ + return Headers( + sec_websocket_accept=self.create_server_nonce(key), + connection="Upgrade", + upgrade="websocket" + ) + + + @classmethod + def check_client_handshake(self, headers): + if headers.get("upgrade") != "websocket": + return + return headers.get("sec-websocket-key") + + + @classmethod + def check_server_handshake(self, headers): + if headers.get("upgrade") != "websocket": + return + return headers.get("sec-websocket-accept") + + + @classmethod + def create_server_nonce(self, client_nonce): + return base64.b64encode(hashlib.sha1(client_nonce + websockets_magic).digest()) diff --git a/netlib/wsgi.py b/netlib/wsgi.py new file mode 100644 index 00000000..d6dfae5d --- /dev/null +++ b/netlib/wsgi.py @@ -0,0 +1,164 @@ +from __future__ import (absolute_import, print_function, division) +from io import BytesIO, StringIO +import urllib +import time +import traceback + +import six +from six.moves import urllib + +from netlib.utils import always_bytes, native +from . import http, tcp + +class ClientConn(object): + + def __init__(self, address): + self.address = tcp.Address.wrap(address) + + +class Flow(object): + + def __init__(self, address, request): + self.client_conn = ClientConn(address) + self.request = request + + +class Request(object): + + def __init__(self, scheme, method, path, http_version, headers, content): + self.scheme, self.method, self.path = scheme, method, path + self.headers, self.content = headers, content + self.http_version = http_version + + +def date_time_string(): + """Return the current date and time formatted for a message header.""" + WEEKS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + MONTHS = [ + None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ] + now = time.time() + year, month, day, hh, mm, ss, wd, y_, z_ = time.gmtime(now) + s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + WEEKS[wd], + day, MONTHS[month], year, + hh, mm, ss + ) + return s + + +class WSGIAdaptor(object): + + def __init__(self, app, domain, port, sversion): + self.app, self.domain, self.port, self.sversion = app, domain, port, sversion + + def make_environ(self, flow, errsoc, **extra): + path = native(flow.request.path, "latin-1") + if '?' in path: + path_info, query = native(path, "latin-1").split('?', 1) + else: + path_info = path + query = '' + environ = { + 'wsgi.version': (1, 0), + 'wsgi.url_scheme': native(flow.request.scheme, "latin-1"), + 'wsgi.input': BytesIO(flow.request.content or b""), + 'wsgi.errors': errsoc, + 'wsgi.multithread': True, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'SERVER_SOFTWARE': self.sversion, + 'REQUEST_METHOD': native(flow.request.method, "latin-1"), + 'SCRIPT_NAME': '', + 'PATH_INFO': urllib.parse.unquote(path_info), + 'QUERY_STRING': query, + 'CONTENT_TYPE': native(flow.request.headers.get('Content-Type', ''), "latin-1"), + 'CONTENT_LENGTH': native(flow.request.headers.get('Content-Length', ''), "latin-1"), + 'SERVER_NAME': self.domain, + 'SERVER_PORT': str(self.port), + 'SERVER_PROTOCOL': native(flow.request.http_version, "latin-1"), + } + environ.update(extra) + if flow.client_conn.address: + environ["REMOTE_ADDR"] = native(flow.client_conn.address.host, "latin-1") + environ["REMOTE_PORT"] = flow.client_conn.address.port + + for key, value in flow.request.headers.items(): + key = 'HTTP_' + native(key, "latin-1").upper().replace('-', '_') + if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): + environ[key] = value + return environ + + def error_page(self, soc, headers_sent, s): + """ + Make a best-effort attempt to write an error page. If headers are + already sent, we just bung the error into the page. + """ + c = """ + +

    Internal Server Error

    +
    {err}"
    + + """.format(err=s).strip().encode() + + if not headers_sent: + soc.write(b"HTTP/1.1 500 Internal Server Error\r\n") + soc.write(b"Content-Type: text/html\r\n") + soc.write("Content-Length: {length}\r\n".format(length=len(c)).encode()) + soc.write(b"\r\n") + soc.write(c) + + def serve(self, request, soc, **env): + state = dict( + response_started=False, + headers_sent=False, + status=None, + headers=None + ) + + def write(data): + if not state["headers_sent"]: + soc.write("HTTP/1.1 {status}\r\n".format(status=state["status"]).encode()) + headers = state["headers"] + if 'server' not in headers: + headers["Server"] = self.sversion + if 'date' not in headers: + headers["Date"] = date_time_string() + soc.write(bytes(headers)) + soc.write(b"\r\n") + state["headers_sent"] = True + if data: + soc.write(data) + soc.flush() + + def start_response(status, headers, exc_info=None): + if exc_info: + if state["headers_sent"]: + six.reraise(*exc_info) + elif state["status"]: + raise AssertionError('Response already started') + state["status"] = status + state["headers"] = http.Headers([[always_bytes(k), always_bytes(v)] for k,v in headers]) + if exc_info: + self.error_page(soc, state["headers_sent"], traceback.format_tb(exc_info[2])) + state["headers_sent"] = True + + errs = six.BytesIO() + try: + dataiter = self.app( + self.make_environ(request, errs, **env), start_response + ) + for i in dataiter: + write(i) + if not state["headers_sent"]: + write(b"") + except Exception as e: + try: + s = traceback.format_exc() + errs.write(s.encode("utf-8", "replace")) + self.error_page(soc, state["headers_sent"], s) + except Exception: # pragma: no cover + pass + return errs.getvalue() diff --git a/pathod/.jsbeautifyrc b/pathod/.jsbeautifyrc deleted file mode 100644 index 725c15ad..00000000 --- a/pathod/.jsbeautifyrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "indent_size": 4, - "indent_char": " ", - "eol": "\n", - "indent_level": 0, - "indent_with_tabs": false, - "preserve_newlines": true, - "max_preserve_newlines": 10, - "jslint_happy": false, - "space_after_anon_function": false, - "brace_style": "collapse", - "keep_array_indentation": false, - "keep_function_indentation": false, - "space_before_conditional": true, - "break_chained_methods": false, - "eval_code": false, - "unescape_strings": false, - "wrap_line_length": 80, - "wrap_attributes": "auto", - "wrap_attributes_indent_size": 4, - "end_with_newline": true -} diff --git a/pathod/README.rst b/pathod/README.rst deleted file mode 100644 index fbedc5ba..00000000 --- a/pathod/README.rst +++ /dev/null @@ -1,60 +0,0 @@ -pathod -^^^^^^ - -|travis| |coveralls| |downloads| |latest_release| |python_versions| - -**pathod** is a collection of pathological tools for testing and torturing HTTP -clients and servers. The project has three components: - -- ``pathod``, an pathological HTTP daemon. -- ``pathoc``, a perverse HTTP client. -- ``pathod.test``, an API for easily using pathod and pathoc in unit tests. - -Installing ----------- - -If you already have **pip** on your system, installing **pathod** and its -dependencies is dead simple: - -.. code-block:: text - - pip install pathod - -Documentation -------------- - -The pathod documentation is self-hosted. Just fire up pathod, like so: - -.. code-block:: text - - ./pathod - -And then browse to: - -``_ - -You can always view the documentation for the latest release at the pathod -website: - -``_ - - -.. |travis| image:: https://shields.mitmproxy.org/travis/mitmproxy/pathod/master.svg - :target: https://travis-ci.org/mitmproxy/pathod - :alt: Build Status - -.. |coveralls| image:: https://shields.mitmproxy.org/coveralls/mitmproxy/pathod/master.svg - :target: https://coveralls.io/r/mitmproxy/pathod - :alt: Coverage Status - -.. |downloads| image:: https://shields.mitmproxy.org/pypi/dm/pathod.svg?color=orange - :target: https://pypi.python.org/pypi/pathod - :alt: Downloads - -.. |latest_release| image:: https://shields.mitmproxy.org/pypi/v/pathod.svg - :target: https://pypi.python.org/pypi/pathod - :alt: Latest Version - -.. |python_versions| image:: https://shields.mitmproxy.org/pypi/pyversions/pathod.svg - :target: https://pypi.python.org/pypi/pathod - :alt: Supported Python versions \ No newline at end of file diff --git a/pathod/__init__.py b/pathod/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pathod/app.py b/pathod/app.py new file mode 100644 index 00000000..c6b7eabc --- /dev/null +++ b/pathod/app.py @@ -0,0 +1,179 @@ +import logging +import pprint +import cStringIO +import copy +from flask import Flask, jsonify, render_template, request, abort, make_response +from . import version, language, utils +from netlib.http import user_agents + +logging.basicConfig(level="DEBUG") +EXAMPLE_HOST = "example.com" +EXAMPLE_WEBSOCKET_KEY = "examplekey" + +# pylint: disable=unused-variable + + +def make_app(noapi, debug): + app = Flask(__name__) + app.debug = debug + + if not noapi: + @app.route('/api/info') + def api_info(): + return jsonify( + version=version.IVERSION + ) + + @app.route('/api/log') + def api_log(): + return jsonify( + log=app.config["pathod"].get_log() + ) + + @app.route('/api/clear_log') + def api_clear_log(): + app.config["pathod"].clear_log() + return "OK" + + def render(s, cacheable, **kwargs): + kwargs["noapi"] = app.config["pathod"].noapi + kwargs["nocraft"] = app.config["pathod"].nocraft + kwargs["craftanchor"] = app.config["pathod"].craftanchor + resp = make_response(render_template(s, **kwargs), 200) + if cacheable: + resp.headers["Cache-control"] = "public, max-age=4320" + return resp + + @app.route('/') + @app.route('/index.html') + def index(): + return render( + "index.html", + True, + section="main", + version=version.VERSION + ) + + @app.route('/download') + @app.route('/download.html') + def download(): + return render( + "download.html", True, section="download", version=version.VERSION + ) + + @app.route('/about') + @app.route('/about.html') + def about(): + return render("about.html", True, section="about") + + @app.route('/docs/pathod') + def docs_pathod(): + return render( + "docs_pathod.html", True, section="docs", subsection="pathod" + ) + + @app.route('/docs/language') + def docs_language(): + return render( + "docs_lang.html", True, + section="docs", uastrings=user_agents.UASTRINGS, + subsection="lang" + ) + + @app.route('/docs/pathoc') + def docs_pathoc(): + return render( + "docs_pathoc.html", True, section="docs", subsection="pathoc" + ) + + @app.route('/docs/lib_pathod') + def docs_lib_pathod(): + return render( + "docs_lib_pathod.html", True, section="docs", subsection="pathod" + ) + + @app.route('/docs/test') + def docs_test(): + return render( + "docs_test.html", True, section="docs", subsection="test" + ) + + @app.route('/log') + def log(): + if app.config["pathod"].noapi: + abort(404) + return render( + "log.html", + False, + section="log", + log=app.config["pathod"].get_log() + ) + + @app.route('/log/') + def onelog(lid): + item = app.config["pathod"].log_by_id(int(lid)) + if not item: + abort(404) + l = pprint.pformat(item) + return render("onelog.html", False, section="log", alog=l, lid=lid) + + def _preview(is_request): + if is_request: + template = "request_preview.html" + else: + template = "response_preview.html" + + spec = request.args["spec"] + + args = dict( + spec=spec, + section="main", + syntaxerror=None, + error=None, + ) + if not spec.strip(): + args["error"] = "Can't parse an empty spec." + return render(template, False, **args) + + try: + if is_request: + r = language.parse_pathoc(spec).next() + else: + r = language.parse_pathod(spec).next() + except language.ParseException as v: + args["syntaxerror"] = str(v) + args["marked"] = v.marked() + return render(template, False, **args) + + s = cStringIO.StringIO() + + settings = copy.copy(app.config["pathod"].settings) + settings.request_host = EXAMPLE_HOST + settings.websocket_key = EXAMPLE_WEBSOCKET_KEY + + safe = r.preview_safe() + err, safe = app.config["pathod"].check_policy( + safe, + settings + ) + if err: + args["error"] = err + return render(template, False, **args) + if is_request: + settings.request_host = EXAMPLE_HOST + language.serve(safe, s, settings) + else: + settings.websocket_key = EXAMPLE_WEBSOCKET_KEY + language.serve(safe, s, settings) + + args["output"] = utils.escape_unprintables(s.getvalue()) + return render(template, False, **args) + + @app.route('/response_preview') + def response_preview(): + return _preview(False) + + @app.route('/request_preview') + def request_preview(): + return _preview(True) + return app diff --git a/pathod/language/__init__.py b/pathod/language/__init__.py new file mode 100644 index 00000000..32199e08 --- /dev/null +++ b/pathod/language/__init__.py @@ -0,0 +1,113 @@ +import itertools +import time + +import pyparsing as pp + +from . import http, http2, websockets, writer, exceptions + +from exceptions import * +from base import Settings +assert Settings # prevent pyflakes from messing with this + + +def expand(msg): + times = getattr(msg, "times", None) + if times: + for j_ in xrange(int(times.value)): + yield msg.strike_token("times") + else: + yield msg + + +def parse_pathod(s, use_http2=False): + """ + May raise ParseException + """ + try: + s = s.decode("ascii") + except UnicodeError: + raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0) + try: + if use_http2: + expressions = [ + # http2.Frame.expr(), + http2.Response.expr(), + ] + else: + expressions = [ + websockets.WebsocketFrame.expr(), + http.Response.expr(), + ] + reqs = pp.Or(expressions).parseString(s, parseAll=True) + except pp.ParseException as v: + raise exceptions.ParseException(v.msg, v.line, v.col) + return itertools.chain(*[expand(i) for i in reqs]) + + +def parse_pathoc(s, use_http2=False): + try: + s = s.decode("ascii") + except UnicodeError: + raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0) + try: + if use_http2: + expressions = [ + # http2.Frame.expr(), + http2.Request.expr(), + ] + else: + expressions = [ + websockets.WebsocketClientFrame.expr(), + http.Request.expr(), + ] + reqs = pp.OneOrMore(pp.Or(expressions)).parseString(s, parseAll=True) + except pp.ParseException as v: + raise exceptions.ParseException(v.msg, v.line, v.col) + return itertools.chain(*[expand(i) for i in reqs]) + + +def parse_websocket_frame(s): + """ + May raise ParseException + """ + try: + reqs = pp.OneOrMore( + websockets.WebsocketFrame.expr() + ).parseString( + s, + parseAll=True + ) + except pp.ParseException as v: + raise exceptions.ParseException(v.msg, v.line, v.col) + return itertools.chain(*[expand(i) for i in reqs]) + + +def serve(msg, fp, settings): + """ + fp: The file pointer to write to. + + request_host: If this a request, this is the connecting host. If + None, we assume it's a response. Used to decide what standard + modifications to make if raw is not set. + + Calling this function may modify the object. + """ + msg = msg.resolve(settings) + started = time.time() + + vals = msg.values(settings) + vals.reverse() + + actions = sorted(msg.actions[:]) + actions.reverse() + actions = [i.intermediate(settings) for i in actions] + + disconnect = writer.write_values(fp, vals, actions[:]) + duration = time.time() - started + ret = dict( + disconnect=disconnect, + started=started, + duration=duration, + ) + ret.update(msg.log(settings)) + return ret diff --git a/pathod/language/actions.py b/pathod/language/actions.py new file mode 100644 index 00000000..34a9bafb --- /dev/null +++ b/pathod/language/actions.py @@ -0,0 +1,126 @@ +import abc +import copy +import random + +import pyparsing as pp + +from . import base + + +class _Action(base.Token): + + """ + An action that operates on the raw data stream of the message. All + actions have one thing in common: an offset that specifies where the + action should take place. + """ + + def __init__(self, offset): + self.offset = offset + + def resolve(self, settings, msg): + """ + Resolves offset specifications to a numeric offset. Returns a copy + of the action object. + """ + c = copy.copy(self) + l = msg.length(settings) + if c.offset == "r": + c.offset = random.randrange(l) + elif c.offset == "a": + c.offset = l + 1 + return c + + def __cmp__(self, other): + return cmp(self.offset, other.offset) + + def __repr__(self): + return self.spec() + + @abc.abstractmethod + def spec(self): # pragma: no cover + pass + + @abc.abstractmethod + def intermediate(self, settings): # pragma: no cover + pass + + +class PauseAt(_Action): + unique_name = None + + def __init__(self, offset, seconds): + _Action.__init__(self, offset) + self.seconds = seconds + + @classmethod + def expr(cls): + e = pp.Literal("p").suppress() + e += base.TokOffset + e += pp.Literal(",").suppress() + e += pp.MatchFirst( + [ + base.v_integer, + pp.Literal("f") + ] + ) + return e.setParseAction(lambda x: cls(*x)) + + def spec(self): + return "p%s,%s" % (self.offset, self.seconds) + + def intermediate(self, settings): + return (self.offset, "pause", self.seconds) + + def freeze(self, settings_): + return self + + +class DisconnectAt(_Action): + + def __init__(self, offset): + _Action.__init__(self, offset) + + @classmethod + def expr(cls): + e = pp.Literal("d").suppress() + e += base.TokOffset + return e.setParseAction(lambda x: cls(*x)) + + def spec(self): + return "d%s" % self.offset + + def intermediate(self, settings): + return (self.offset, "disconnect") + + def freeze(self, settings_): + return self + + +class InjectAt(_Action): + unique_name = None + + def __init__(self, offset, value): + _Action.__init__(self, offset) + self.value = value + + @classmethod + def expr(cls): + e = pp.Literal("i").suppress() + e += base.TokOffset + e += pp.Literal(",").suppress() + e += base.TokValue + return e.setParseAction(lambda x: cls(*x)) + + def spec(self): + return "i%s,%s" % (self.offset, self.value.spec()) + + def intermediate(self, settings): + return ( + self.offset, + "inject", + self.value.get_generator(settings) + ) + + def freeze(self, settings): + return InjectAt(self.offset, self.value.freeze(settings)) diff --git a/pathod/language/base.py b/pathod/language/base.py new file mode 100644 index 00000000..a4302998 --- /dev/null +++ b/pathod/language/base.py @@ -0,0 +1,576 @@ +import operator +import os +import abc +import pyparsing as pp + +from .. import utils +from . import generators, exceptions + +class Settings(object): + + def __init__( + self, + is_client=False, + staticdir=None, + unconstrained_file_access=False, + request_host=None, + websocket_key=None, + protocol=None, + ): + self.is_client = is_client + self.staticdir = staticdir + self.unconstrained_file_access = unconstrained_file_access + self.request_host = request_host + self.websocket_key = websocket_key # TODO: refactor this into the protocol + self.protocol = protocol + + +Sep = pp.Optional(pp.Literal(":")).suppress() + + +v_integer = pp.Word(pp.nums)\ + .setName("integer")\ + .setParseAction(lambda toks: int(toks[0])) + + +v_literal = pp.MatchFirst( + [ + pp.QuotedString( + "\"", + unquoteResults=True, + multiline=True + ), + pp.QuotedString( + "'", + unquoteResults=True, + multiline=True + ), + ] +) + +v_naked_literal = pp.MatchFirst( + [ + v_literal, + pp.Word("".join(i for i in pp.printables if i not in ",:\n@\'\"")) + ] +) + + +class Token(object): + + """ + A token in the specification language. Tokens are immutable. The token + classes have no meaning in and of themselves, and are combined into + Components and Actions to build the language. + """ + __metaclass__ = abc.ABCMeta + + @classmethod + def expr(cls): # pragma: no cover + """ + A parse expression. + """ + return None + + @abc.abstractmethod + def spec(self): # pragma: no cover + """ + A parseable specification for this token. + """ + return None + + @property + def unique_name(self): + """ + Controls uniqueness constraints for tokens. No two tokens with the + same name will be allowed. If no uniquness should be applied, this + should be None. + """ + return self.__class__.__name__.lower() + + def resolve(self, settings_, msg_): + """ + Resolves this token to ready it for transmission. This means that + the calculated offsets of actions are fixed. + + settings: a language.Settings instance + msg: The containing message + """ + return self + + def __repr__(self): + return self.spec() + + +class _TokValueLiteral(Token): + + def __init__(self, val): + self.val = val.decode("string_escape") + + def get_generator(self, settings_): + return self.val + + def freeze(self, settings_): + return self + + +class TokValueLiteral(_TokValueLiteral): + + """ + A literal with Python-style string escaping + """ + @classmethod + def expr(cls): + e = v_literal.copy() + return e.setParseAction(cls.parseAction) + + @classmethod + def parseAction(cls, x): + v = cls(*x) + return v + + def spec(self): + inner = self.val.encode("string_escape") + inner = inner.replace(r"\'", r"\x27") + return "'" + inner + "'" + + +class TokValueNakedLiteral(_TokValueLiteral): + + @classmethod + def expr(cls): + e = v_naked_literal.copy() + return e.setParseAction(lambda x: cls(*x)) + + def spec(self): + return self.val.encode("string_escape") + + +class TokValueGenerate(Token): + + def __init__(self, usize, unit, datatype): + if not unit: + unit = "b" + self.usize, self.unit, self.datatype = usize, unit, datatype + + def bytes(self): + return self.usize * utils.SIZE_UNITS[self.unit] + + def get_generator(self, settings_): + return generators.RandomGenerator(self.datatype, self.bytes()) + + def freeze(self, settings): + g = self.get_generator(settings) + return TokValueLiteral(g[:].encode("string_escape")) + + @classmethod + def expr(cls): + e = pp.Literal("@").suppress() + v_integer + + u = reduce( + operator.or_, + [pp.Literal(i) for i in utils.SIZE_UNITS.keys()] + ).leaveWhitespace() + e = e + pp.Optional(u, default=None) + + s = pp.Literal(",").suppress() + s += reduce( + operator.or_, + [pp.Literal(i) for i in generators.DATATYPES.keys()] + ) + e += pp.Optional(s, default="bytes") + return e.setParseAction(lambda x: cls(*x)) + + def spec(self): + s = "@%s" % self.usize + if self.unit != "b": + s += self.unit + if self.datatype != "bytes": + s += ",%s" % self.datatype + return s + + +class TokValueFile(Token): + + def __init__(self, path): + self.path = str(path) + + @classmethod + def expr(cls): + e = pp.Literal("<").suppress() + e = e + v_naked_literal + return e.setParseAction(lambda x: cls(*x)) + + def freeze(self, settings_): + return self + + def get_generator(self, settings): + if not settings.staticdir: + raise exceptions.FileAccessDenied("File access disabled.") + s = os.path.expanduser(self.path) + s = os.path.normpath( + os.path.abspath(os.path.join(settings.staticdir, s)) + ) + uf = settings.unconstrained_file_access + if not uf and not s.startswith(settings.staticdir): + raise exceptions.FileAccessDenied( + "File access outside of configured directory" + ) + if not os.path.isfile(s): + raise exceptions.FileAccessDenied("File not readable") + return generators.FileGenerator(s) + + def spec(self): + return "<'%s'" % self.path.encode("string_escape") + + +TokValue = pp.MatchFirst( + [ + TokValueGenerate.expr(), + TokValueFile.expr(), + TokValueLiteral.expr() + ] +) + + +TokNakedValue = pp.MatchFirst( + [ + TokValueGenerate.expr(), + TokValueFile.expr(), + TokValueLiteral.expr(), + TokValueNakedLiteral.expr(), + ] +) + + +TokOffset = pp.MatchFirst( + [ + v_integer, + pp.Literal("r"), + pp.Literal("a") + ] +) + + +class _Component(Token): + + """ + A value component of the primary specification of an message. + Components produce byte values desribe the bytes of the message. + """ + + def values(self, settings): # pragma: no cover + """ + A sequence of values, which can either be strings or generators. + """ + pass + + def string(self, settings=None): + """ + A string representation of the object. + """ + return "".join(i[:] for i in self.values(settings or {})) + + +class KeyValue(_Component): + + """ + A key/value pair. + cls.preamble: leader + """ + + def __init__(self, key, value): + self.key, self.value = key, value + + @classmethod + def expr(cls): + e = pp.Literal(cls.preamble).suppress() + e += TokValue + e += pp.Literal("=").suppress() + e += TokValue + return e.setParseAction(lambda x: cls(*x)) + + def spec(self): + return "%s%s=%s" % (self.preamble, self.key.spec(), self.value.spec()) + + def freeze(self, settings): + return self.__class__( + self.key.freeze(settings), self.value.freeze(settings) + ) + + +class CaselessLiteral(_Component): + + """ + A caseless token that can take only one value. + """ + + def __init__(self, value): + self.value = value + + @classmethod + def expr(cls): + spec = pp.CaselessLiteral(cls.TOK) + spec = spec.setParseAction(lambda x: cls(*x)) + return spec + + def values(self, settings): + return self.TOK + + def spec(self): + return self.TOK + + def freeze(self, settings_): + return self + + +class OptionsOrValue(_Component): + + """ + Can be any of a specified set of options, or a value specifier. + """ + preamble = "" + options = [] + + def __init__(self, value): + # If it's a string, we were passed one of the options, so we lower-case + # it to be canonical. The user can specify a different case by using a + # string value literal. + self.option_used = False + if isinstance(value, basestring): + for i in self.options: + # Find the exact option value in a case-insensitive way + if i.lower() == value.lower(): + self.option_used = True + value = TokValueLiteral(i) + break + self.value = value + + @classmethod + def expr(cls): + parts = [pp.CaselessLiteral(i) for i in cls.options] + m = pp.MatchFirst(parts) + spec = m | TokValue.copy() + spec = spec.setParseAction(lambda x: cls(*x)) + if cls.preamble: + spec = pp.Literal(cls.preamble).suppress() + spec + return spec + + def values(self, settings): + return [ + self.value.get_generator(settings) + ] + + def spec(self): + s = self.value.spec() + if s[1:-1].lower() in self.options: + s = s[1:-1].lower() + return "%s%s" % (self.preamble, s) + + def freeze(self, settings): + return self.__class__(self.value.freeze(settings)) + + +class Integer(_Component): + bounds = (None, None) + preamble = "" + + def __init__(self, value): + v = int(value) + outofbounds = any([ + self.bounds[0] is not None and v < self.bounds[0], + self.bounds[1] is not None and v > self.bounds[1] + ]) + if outofbounds: + raise exceptions.ParseException( + "Integer value must be between %s and %s." % self.bounds, + 0, 0 + ) + self.value = str(value) + + @classmethod + def expr(cls): + e = v_integer.copy() + if cls.preamble: + e = pp.Literal(cls.preamble).suppress() + e + return e.setParseAction(lambda x: cls(*x)) + + def values(self, settings): + return self.value + + def spec(self): + return "%s%s" % (self.preamble, self.value) + + def freeze(self, settings_): + return self + + +class Value(_Component): + + """ + A value component lead by an optional preamble. + """ + preamble = "" + + def __init__(self, value): + self.value = value + + @classmethod + def expr(cls): + e = (TokValue | TokNakedValue) + if cls.preamble: + e = pp.Literal(cls.preamble).suppress() + e + return e.setParseAction(lambda x: cls(*x)) + + def values(self, settings): + return [self.value.get_generator(settings)] + + def spec(self): + return "%s%s" % (self.preamble, self.value.spec()) + + def freeze(self, settings): + return self.__class__(self.value.freeze(settings)) + + +class FixedLengthValue(Value): + + """ + A value component lead by an optional preamble. + """ + preamble = "" + length = None + + def __init__(self, value): + Value.__init__(self, value) + lenguess = None + try: + lenguess = len(value.get_generator(Settings())) + except exceptions.RenderError: + pass + # This check will fail if we know the length upfront + if lenguess is not None and lenguess != self.length: + raise exceptions.RenderError( + "Invalid value length: '%s' is %s bytes, should be %s." % ( + self.spec(), + lenguess, + self.length + ) + ) + + def values(self, settings): + ret = Value.values(self, settings) + l = sum(len(i) for i in ret) + # This check will fail if we don't know the length upfront - i.e. for + # file inputs + if l != self.length: + raise exceptions.RenderError( + "Invalid value length: '%s' is %s bytes, should be %s." % ( + self.spec(), + l, + self.length + ) + ) + return ret + + +class Boolean(_Component): + + """ + A boolean flag. + name = true + -name = false + """ + name = "" + + def __init__(self, value): + self.value = value + + @classmethod + def expr(cls): + e = pp.Optional(pp.Literal("-"), default=True) + e += pp.Literal(cls.name).suppress() + + def parse(s_, loc_, toks): + val = True + if toks[0] == "-": + val = False + return cls(val) + + return e.setParseAction(parse) + + def spec(self): + return "%s%s" % ("-" if not self.value else "", self.name) + + +class IntField(_Component): + + """ + An integer field, where values can optionally specified by name. + """ + names = {} + max = 16 + preamble = "" + + def __init__(self, value): + self.origvalue = value + self.value = self.names.get(value, value) + if self.value > self.max: + raise exceptions.ParseException( + "Value can't exceed %s" % self.max, 0, 0 + ) + + @classmethod + def expr(cls): + parts = [pp.CaselessLiteral(i) for i in cls.names.keys()] + m = pp.MatchFirst(parts) + spec = m | v_integer.copy() + spec = spec.setParseAction(lambda x: cls(*x)) + if cls.preamble: + spec = pp.Literal(cls.preamble).suppress() + spec + return spec + + def values(self, settings): + return [str(self.value)] + + def spec(self): + return "%s%s" % (self.preamble, self.origvalue) + + +class NestedMessage(Token): + + """ + A nested message, as an escaped string with a preamble. + """ + preamble = "" + nest_type = None + + def __init__(self, value): + Token.__init__(self) + self.value = value + try: + self.parsed = self.nest_type( + self.nest_type.expr().parseString( + value.val, + parseAll=True + ) + ) + except pp.ParseException as v: + raise exceptions.ParseException(v.msg, v.line, v.col) + + @classmethod + def expr(cls): + e = pp.Literal(cls.preamble).suppress() + e = e + TokValueLiteral.expr() + return e.setParseAction(lambda x: cls(*x)) + + def values(self, settings): + return [ + self.value.get_generator(settings), + ] + + def spec(self): + return "%s%s" % (self.preamble, self.value.spec()) + + def freeze(self, settings): + f = self.parsed.freeze(settings).spec() + return self.__class__(TokValueLiteral(f.encode("string_escape"))) diff --git a/pathod/language/exceptions.py b/pathod/language/exceptions.py new file mode 100644 index 00000000..84ad3c02 --- /dev/null +++ b/pathod/language/exceptions.py @@ -0,0 +1,22 @@ + +class RenderError(Exception): + pass + + +class FileAccessDenied(RenderError): + pass + + +class ParseException(Exception): + + def __init__(self, msg, s, col): + Exception.__init__(self) + self.msg = msg + self.s = s + self.col = col + + def marked(self): + return "%s\n%s" % (self.s, " " * (self.col - 1) + "^") + + def __str__(self): + return "%s at char %s" % (self.msg, self.col) diff --git a/pathod/language/generators.py b/pathod/language/generators.py new file mode 100644 index 00000000..a17e7052 --- /dev/null +++ b/pathod/language/generators.py @@ -0,0 +1,86 @@ +import string +import random +import mmap + +DATATYPES = dict( + ascii_letters=string.ascii_letters, + ascii_lowercase=string.ascii_lowercase, + ascii_uppercase=string.ascii_uppercase, + digits=string.digits, + hexdigits=string.hexdigits, + octdigits=string.octdigits, + punctuation=string.punctuation, + whitespace=string.whitespace, + ascii=string.printable, + bytes="".join(chr(i) for i in range(256)) +) + + +class TransformGenerator(object): + + """ + Perform a byte-by-byte transform another generator - that is, for each + input byte, the transformation must produce one output byte. + + gen: A generator to wrap + transform: A function (offset, data) -> transformed + """ + + def __init__(self, gen, transform): + self.gen = gen + self.transform = transform + + def __len__(self): + return len(self.gen) + + def __getitem__(self, x): + d = self.gen.__getitem__(x) + return self.transform(x, d) + + def __getslice__(self, a, b): + d = self.gen.__getslice__(a, b) + return self.transform(a, d) + + def __repr__(self): + return "'transform(%s)'" % self.gen + + +class RandomGenerator(object): + + def __init__(self, dtype, length): + self.dtype = dtype + self.length = length + + def __len__(self): + return self.length + + def __getitem__(self, x): + return random.choice(DATATYPES[self.dtype]) + + def __getslice__(self, a, b): + b = min(b, self.length) + chars = DATATYPES[self.dtype] + return "".join(random.choice(chars) for x in range(a, b)) + + def __repr__(self): + return "%s random from %s" % (self.length, self.dtype) + + +class FileGenerator(object): + + def __init__(self, path): + self.path = path + self.fp = file(path, "rb") + self.map = mmap.mmap(self.fp.fileno(), 0, access=mmap.ACCESS_READ) + + def __len__(self): + return len(self.map) + + def __getitem__(self, x): + return self.map.__getitem__(x) + + def __getslice__(self, a, b): + return self.map.__getslice__(a, b) + + def __repr__(self): + return "<%s" % self.path diff --git a/pathod/language/http.py b/pathod/language/http.py new file mode 100644 index 00000000..a82f12fe --- /dev/null +++ b/pathod/language/http.py @@ -0,0 +1,381 @@ + +import abc + +import pyparsing as pp + +import netlib.websockets +from netlib.http import status_codes, user_agents +from . import base, exceptions, actions, message + +# TODO: use netlib.semantics.protocol assemble method, +# instead of duplicating the HTTP on-the-wire representation here. +# see http2 language for an example + +class WS(base.CaselessLiteral): + TOK = "ws" + + +class Raw(base.CaselessLiteral): + TOK = "r" + + +class Path(base.Value): + pass + + +class StatusCode(base.Integer): + pass + + +class Reason(base.Value): + preamble = "m" + + +class Body(base.Value): + preamble = "b" + + +class Times(base.Integer): + preamble = "x" + + +class Method(base.OptionsOrValue): + options = [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "OPTIONS", + "TRACE", + "CONNECT", + ] + + +class _HeaderMixin(object): + unique_name = None + + def format_header(self, key, value): + return [key, ": ", value, "\r\n"] + + def values(self, settings): + return self.format_header( + self.key.get_generator(settings), + self.value.get_generator(settings), + ) + + +class Header(_HeaderMixin, base.KeyValue): + preamble = "h" + + +class ShortcutContentType(_HeaderMixin, base.Value): + preamble = "c" + key = base.TokValueLiteral("Content-Type") + + +class ShortcutLocation(_HeaderMixin, base.Value): + preamble = "l" + key = base.TokValueLiteral("Location") + + +class ShortcutUserAgent(_HeaderMixin, base.OptionsOrValue): + preamble = "u" + options = [i[1] for i in user_agents.UASTRINGS] + key = base.TokValueLiteral("User-Agent") + + def values(self, settings): + value = self.value.val + if self.option_used: + value = user_agents.get_by_shortcut(value.lower())[2] + + return self.format_header( + self.key.get_generator(settings), + value + ) + + +def get_header(val, headers): + """ + Header keys may be Values, so we have to "generate" them as we try the + match. + """ + for h in headers: + k = h.key.get_generator({}) + if len(k) == len(val) and k[:].lower() == val.lower(): + return h + return None + + +class _HTTPMessage(message.Message): + version = "HTTP/1.1" + + @property + def actions(self): + return self.toks(actions._Action) + + @property + def raw(self): + return bool(self.tok(Raw)) + + @property + def body(self): + return self.tok(Body) + + @abc.abstractmethod + def preamble(self, settings): # pragma: no cover + pass + + @property + def headers(self): + return self.toks(_HeaderMixin) + + def values(self, settings): + vals = self.preamble(settings) + vals.append("\r\n") + for h in self.headers: + vals.extend(h.values(settings)) + vals.append("\r\n") + if self.body: + vals.extend(self.body.values(settings)) + return vals + + +class Response(_HTTPMessage): + unique_name = None + comps = ( + Header, + ShortcutContentType, + ShortcutLocation, + Raw, + Reason, + Body, + + actions.PauseAt, + actions.DisconnectAt, + actions.InjectAt, + ) + logattrs = ["status_code", "reason", "version", "body"] + + @property + def ws(self): + return self.tok(WS) + + @property + def status_code(self): + return self.tok(StatusCode) + + @property + def reason(self): + return self.tok(Reason) + + def preamble(self, settings): + l = [self.version, " "] + l.extend(self.status_code.values(settings)) + status_code = int(self.status_code.value) + l.append(" ") + if self.reason: + l.extend(self.reason.values(settings)) + else: + l.append( + status_codes.RESPONSES.get( + status_code, + "Unknown code" + ) + ) + return l + + def resolve(self, settings, msg=None): + tokens = self.tokens[:] + if self.ws: + if not settings.websocket_key: + raise exceptions.RenderError( + "No websocket key - have we seen a client handshake?" + ) + if not self.status_code: + tokens.insert( + 1, + StatusCode(101) + ) + headers = netlib.websockets.WebsocketsProtocol.server_handshake_headers( + settings.websocket_key + ) + for i in headers.fields: + if not get_header(i[0], self.headers): + tokens.append( + Header( + base.TokValueLiteral(i[0]), + base.TokValueLiteral(i[1])) + ) + if not self.raw: + if not get_header("Content-Length", self.headers): + if not self.body: + length = 0 + else: + length = sum( + len(i) for i in self.body.values(settings) + ) + tokens.append( + Header( + base.TokValueLiteral("Content-Length"), + base.TokValueLiteral(str(length)), + ) + ) + intermediate = self.__class__(tokens) + return self.__class__( + [i.resolve(settings, intermediate) for i in tokens] + ) + + @classmethod + def expr(cls): + parts = [i.expr() for i in cls.comps] + atom = pp.MatchFirst(parts) + resp = pp.And( + [ + pp.MatchFirst( + [ + WS.expr() + pp.Optional( + base.Sep + StatusCode.expr() + ), + StatusCode.expr(), + ] + ), + pp.ZeroOrMore(base.Sep + atom) + ] + ) + resp = resp.setParseAction(cls) + return resp + + def spec(self): + return ":".join([i.spec() for i in self.tokens]) + + +class NestedResponse(base.NestedMessage): + preamble = "s" + nest_type = Response + + +class Request(_HTTPMessage): + comps = ( + Header, + ShortcutContentType, + ShortcutUserAgent, + Raw, + NestedResponse, + Body, + Times, + + actions.PauseAt, + actions.DisconnectAt, + actions.InjectAt, + ) + logattrs = ["method", "path", "body"] + + @property + def ws(self): + return self.tok(WS) + + @property + def method(self): + return self.tok(Method) + + @property + def path(self): + return self.tok(Path) + + @property + def times(self): + return self.tok(Times) + + @property + def nested_response(self): + return self.tok(NestedResponse) + + def preamble(self, settings): + v = self.method.values(settings) + v.append(" ") + v.extend(self.path.values(settings)) + if self.nested_response: + v.append(self.nested_response.parsed.spec()) + v.append(" ") + v.append(self.version) + return v + + def resolve(self, settings, msg=None): + tokens = self.tokens[:] + if self.ws: + if not self.method: + tokens.insert( + 1, + Method("get") + ) + for i in netlib.websockets.WebsocketsProtocol.client_handshake_headers().fields: + if not get_header(i[0], self.headers): + tokens.append( + Header( + base.TokValueLiteral(i[0]), + base.TokValueLiteral(i[1]) + ) + ) + if not self.raw: + if not get_header("Content-Length", self.headers): + if self.body: + length = sum( + len(i) for i in self.body.values(settings) + ) + tokens.append( + Header( + base.TokValueLiteral("Content-Length"), + base.TokValueLiteral(str(length)), + ) + ) + if settings.request_host: + if not get_header("Host", self.headers): + tokens.append( + Header( + base.TokValueLiteral("Host"), + base.TokValueLiteral(settings.request_host) + ) + ) + intermediate = self.__class__(tokens) + return self.__class__( + [i.resolve(settings, intermediate) for i in tokens] + ) + + @classmethod + def expr(cls): + parts = [i.expr() for i in cls.comps] + atom = pp.MatchFirst(parts) + resp = pp.And( + [ + pp.MatchFirst( + [ + WS.expr() + pp.Optional( + base.Sep + Method.expr() + ), + Method.expr(), + ] + ), + base.Sep, + Path.expr(), + pp.ZeroOrMore(base.Sep + atom) + ] + ) + resp = resp.setParseAction(cls) + return resp + + def spec(self): + return ":".join([i.spec() for i in self.tokens]) + + +def make_error_response(reason, body=None): + tokens = [ + StatusCode("800"), + Header( + base.TokValueLiteral("Content-Type"), + base.TokValueLiteral("text/plain") + ), + Reason(base.TokValueLiteral(reason)), + Body(base.TokValueLiteral("pathod error: " + (body or reason))), + ] + return Response(tokens) diff --git a/pathod/language/http2.py b/pathod/language/http2.py new file mode 100644 index 00000000..d5e3ca31 --- /dev/null +++ b/pathod/language/http2.py @@ -0,0 +1,299 @@ +import pyparsing as pp + +from netlib import http +from netlib.http import user_agents, Headers +from . import base, message + +""" + Normal HTTP requests: + ::
    : + e.g.: + GET:/ + GET:/:h"foo"="bar" + POST:/:h"foo"="bar":b'content body payload' + + Normal HTTP responses: + :
    : + e.g.: + 200 + 302:h"foo"="bar" + 404:h"foo"="bar":b'content body payload' + + Individual HTTP/2 frames: + h2f::::: + e.g.: + h2f:0:PING + h2f:42:HEADERS:END_HEADERS:0x1234567:foo=bar,host=example.com + h2f:42:DATA:END_STREAM,PADDED:0x1234567:'content body payload' +""" + +def get_header(val, headers): + """ + Header keys may be Values, so we have to "generate" them as we try the + match. + """ + for h in headers: + k = h.key.get_generator({}) + if len(k) == len(val) and k[:].lower() == val.lower(): + return h + return None + + +class _HeaderMixin(object): + unique_name = None + + def values(self, settings): + return ( + self.key.get_generator(settings), + self.value.get_generator(settings), + ) + +class _HTTP2Message(message.Message): + @property + def actions(self): + return [] # self.toks(actions._Action) + + @property + def headers(self): + headers = self.toks(_HeaderMixin) + + if not self.raw: + if not get_header("content-length", headers): + if not self.body: + length = 0 + else: + length = len(self.body.string()) + headers.append( + Header( + base.TokValueLiteral("content-length"), + base.TokValueLiteral(str(length)), + ) + ) + return headers + + @property + def raw(self): + return bool(self.tok(Raw)) + + @property + def body(self): + return self.tok(Body) + + def resolve(self, settings): + return self + + +class StatusCode(base.Integer): + pass + + +class Method(base.OptionsOrValue): + options = [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + ] + + +class Path(base.Value): + pass + + +class Header(_HeaderMixin, base.KeyValue): + preamble = "h" + + +class ShortcutContentType(_HeaderMixin, base.Value): + preamble = "c" + key = base.TokValueLiteral("content-type") + + +class ShortcutLocation(_HeaderMixin, base.Value): + preamble = "l" + key = base.TokValueLiteral("location") + + +class ShortcutUserAgent(_HeaderMixin, base.OptionsOrValue): + preamble = "u" + options = [i[1] for i in user_agents.UASTRINGS] + key = base.TokValueLiteral("user-agent") + + def values(self, settings): + value = self.value.val + if self.option_used: + value = user_agents.get_by_shortcut(value.lower())[2] + + return ( + self.key.get_generator(settings), + value + ) + + +class Raw(base.CaselessLiteral): + TOK = "r" + + +class Body(base.Value): + preamble = "b" + + +class Times(base.Integer): + preamble = "x" + + +class Response(_HTTP2Message): + unique_name = None + comps = ( + Header, + Body, + ShortcutContentType, + ShortcutLocation, + Raw, + ) + + def __init__(self, tokens): + super(Response, self).__init__(tokens) + self.rendered_values = None + self.stream_id = 2 + + @property + def status_code(self): + return self.tok(StatusCode) + + @classmethod + def expr(cls): + parts = [i.expr() for i in cls.comps] + atom = pp.MatchFirst(parts) + resp = pp.And( + [ + StatusCode.expr(), + pp.ZeroOrMore(base.Sep + atom) + ] + ) + resp = resp.setParseAction(cls) + return resp + + def values(self, settings): + if self.rendered_values: + return self.rendered_values + else: + headers = Headers([header.values(settings) for header in self.headers]) + + body = self.body + if body: + body = body.string() + + resp = http.Response( + (2, 0), + self.status_code.string(), + '', + headers, + body, + ) + resp.stream_id = self.stream_id + + self.rendered_values = settings.protocol.assemble(resp) + return self.rendered_values + + def spec(self): + return ":".join([i.spec() for i in self.tokens]) + + +class NestedResponse(base.NestedMessage): + preamble = "s" + nest_type = Response + + +class Request(_HTTP2Message): + comps = ( + Header, + ShortcutContentType, + ShortcutUserAgent, + Raw, + NestedResponse, + Body, + Times, + ) + logattrs = ["method", "path"] + + def __init__(self, tokens): + super(Request, self).__init__(tokens) + self.rendered_values = None + self.stream_id = 1 + + @property + def method(self): + return self.tok(Method) + + @property + def path(self): + return self.tok(Path) + + @property + def nested_response(self): + return self.tok(NestedResponse) + + @property + def times(self): + return self.tok(Times) + + @classmethod + def expr(cls): + parts = [i.expr() for i in cls.comps] + atom = pp.MatchFirst(parts) + resp = pp.And( + [ + Method.expr(), + base.Sep, + Path.expr(), + pp.ZeroOrMore(base.Sep + atom) + ] + ) + resp = resp.setParseAction(cls) + return resp + + def values(self, settings): + if self.rendered_values: + return self.rendered_values + else: + path = self.path.string() + if self.nested_response: + path += self.nested_response.parsed.spec() + + headers = Headers([header.values(settings) for header in self.headers]) + + body = self.body + if body: + body = body.string() + + req = http.Request( + '', + self.method.string(), + '', + '', + '', + path, + (2, 0), + headers, + body, + ) + req.stream_id = self.stream_id + + self.rendered_values = settings.protocol.assemble(req) + return self.rendered_values + + def spec(self): + return ":".join([i.spec() for i in self.tokens]) + +def make_error_response(reason, body=None): + tokens = [ + StatusCode("800"), + Body(base.TokValueLiteral("pathod error: " + (body or reason))), + ] + return Response(tokens) + + +# class Frame(message.Message): +# pass diff --git a/pathod/language/message.py b/pathod/language/message.py new file mode 100644 index 00000000..33124856 --- /dev/null +++ b/pathod/language/message.py @@ -0,0 +1,96 @@ +import abc +from . import actions, exceptions + +LOG_TRUNCATE = 1024 + + +class Message(object): + __metaclass__ = abc.ABCMeta + logattrs = [] + + def __init__(self, tokens): + track = set([]) + for i in tokens: + if i.unique_name: + if i.unique_name in track: + raise exceptions.ParseException( + "Message has multiple %s clauses, " + "but should only have one." % i.unique_name, + 0, 0 + ) + else: + track.add(i.unique_name) + self.tokens = tokens + + def strike_token(self, name): + toks = [i for i in self.tokens if i.unique_name != name] + return self.__class__(toks) + + def toks(self, klass): + """ + Fetch all tokens that are instances of klass + """ + return [i for i in self.tokens if isinstance(i, klass)] + + def tok(self, klass): + """ + Fetch first token that is an instance of klass + """ + l = self.toks(klass) + if l: + return l[0] + + def length(self, settings): + """ + Calculate the length of the base message without any applied + actions. + """ + return sum(len(x) for x in self.values(settings)) + + def preview_safe(self): + """ + Return a copy of this message that issafe for previews. + """ + tokens = [i for i in self.tokens if not isinstance(i, actions.PauseAt)] + return self.__class__(tokens) + + def maximum_length(self, settings): + """ + Calculate the maximum length of the base message with all applied + actions. + """ + l = self.length(settings) + for i in self.actions: + if isinstance(i, actions.InjectAt): + l += len(i.value.get_generator(settings)) + return l + + @classmethod + def expr(cls): # pragma: no cover + pass + + def log(self, settings): + """ + A dictionary that should be logged if this message is served. + """ + ret = {} + for i in self.logattrs: + v = getattr(self, i) + # Careful not to log any VALUE specs without sanitizing them first. + # We truncate at 1k. + if hasattr(v, "values"): + v = [x[:LOG_TRUNCATE] for x in v.values(settings)] + v = "".join(v).encode("string_escape") + elif hasattr(v, "__len__"): + v = v[:LOG_TRUNCATE] + v = v.encode("string_escape") + ret[i] = v + ret["spec"] = self.spec() + return ret + + def freeze(self, settings): + r = self.resolve(settings) + return self.__class__([i.freeze(settings) for i in r.tokens]) + + def __repr__(self): + return self.spec() diff --git a/pathod/language/websockets.py b/pathod/language/websockets.py new file mode 100644 index 00000000..ea7c870e --- /dev/null +++ b/pathod/language/websockets.py @@ -0,0 +1,241 @@ +import os +import netlib.websockets +import pyparsing as pp +from . import base, generators, actions, message + +NESTED_LEADER = "pathod!" + + +class WF(base.CaselessLiteral): + TOK = "wf" + + +class OpCode(base.IntField): + names = { + "continue": netlib.websockets.OPCODE.CONTINUE, + "text": netlib.websockets.OPCODE.TEXT, + "binary": netlib.websockets.OPCODE.BINARY, + "close": netlib.websockets.OPCODE.CLOSE, + "ping": netlib.websockets.OPCODE.PING, + "pong": netlib.websockets.OPCODE.PONG, + } + max = 15 + preamble = "c" + + +class Body(base.Value): + preamble = "b" + + +class RawBody(base.Value): + unique_name = "body" + preamble = "r" + + +class Fin(base.Boolean): + name = "fin" + + +class RSV1(base.Boolean): + name = "rsv1" + + +class RSV2(base.Boolean): + name = "rsv2" + + +class RSV3(base.Boolean): + name = "rsv3" + + +class Mask(base.Boolean): + name = "mask" + + +class Key(base.FixedLengthValue): + preamble = "k" + length = 4 + + +class KeyNone(base.CaselessLiteral): + unique_name = "key" + TOK = "knone" + + +class Length(base.Integer): + bounds = (0, 1 << 64) + preamble = "l" + + +class Times(base.Integer): + preamble = "x" + + +COMPONENTS = ( + OpCode, + Length, + # Bit flags + Fin, + RSV1, + RSV2, + RSV3, + Mask, + actions.PauseAt, + actions.DisconnectAt, + actions.InjectAt, + KeyNone, + Key, + Times, + + Body, + RawBody, +) + + +class WebsocketFrame(message.Message): + components = COMPONENTS + logattrs = ["body"] + # Used for nested frames + unique_name = "body" + + @property + def actions(self): + return self.toks(actions._Action) + + @property + def body(self): + return self.tok(Body) + + @property + def rawbody(self): + return self.tok(RawBody) + + @property + def opcode(self): + return self.tok(OpCode) + + @property + def fin(self): + return self.tok(Fin) + + @property + def rsv1(self): + return self.tok(RSV1) + + @property + def rsv2(self): + return self.tok(RSV2) + + @property + def rsv3(self): + return self.tok(RSV3) + + @property + def mask(self): + return self.tok(Mask) + + @property + def key(self): + return self.tok(Key) + + @property + def knone(self): + return self.tok(KeyNone) + + @property + def times(self): + return self.tok(Times) + + @property + def toklength(self): + return self.tok(Length) + + @classmethod + def expr(cls): + parts = [i.expr() for i in cls.components] + atom = pp.MatchFirst(parts) + resp = pp.And( + [ + WF.expr(), + base.Sep, + pp.ZeroOrMore(base.Sep + atom) + ] + ) + resp = resp.setParseAction(cls) + return resp + + @property + def nested_frame(self): + return self.tok(NestedFrame) + + def resolve(self, settings, msg=None): + tokens = self.tokens[:] + if not self.mask and settings.is_client: + tokens.append( + Mask(True) + ) + if not self.knone and self.mask and self.mask.value and not self.key: + tokens.append( + Key(base.TokValueLiteral(os.urandom(4))) + ) + return self.__class__( + [i.resolve(settings, self) for i in tokens] + ) + + def values(self, settings): + if self.body: + bodygen = self.body.value.get_generator(settings) + length = len(self.body.value.get_generator(settings)) + elif self.rawbody: + bodygen = self.rawbody.value.get_generator(settings) + length = len(self.rawbody.value.get_generator(settings)) + elif self.nested_frame: + bodygen = NESTED_LEADER + self.nested_frame.parsed.spec() + length = len(bodygen) + else: + bodygen = None + length = 0 + if self.toklength: + length = int(self.toklength.value) + frameparts = dict( + payload_length=length + ) + if self.mask and self.mask.value: + frameparts["mask"] = True + if self.knone: + frameparts["masking_key"] = None + elif self.key: + key = self.key.values(settings)[0][:] + frameparts["masking_key"] = key + for i in ["opcode", "fin", "rsv1", "rsv2", "rsv3", "mask"]: + v = getattr(self, i, None) + if v is not None: + frameparts[i] = v.value + frame = netlib.websockets.FrameHeader(**frameparts) + vals = [bytes(frame)] + if bodygen: + if frame.masking_key and not self.rawbody: + masker = netlib.websockets.Masker(frame.masking_key) + vals.append( + generators.TransformGenerator( + bodygen, + masker.mask + ) + ) + else: + vals.append(bodygen) + return vals + + def spec(self): + return ":".join([i.spec() for i in self.tokens]) + + +class NestedFrame(base.NestedMessage): + preamble = "f" + nest_type = WebsocketFrame + + +class WebsocketClientFrame(WebsocketFrame): + components = COMPONENTS + ( + NestedFrame, + ) diff --git a/pathod/language/writer.py b/pathod/language/writer.py new file mode 100644 index 00000000..1a27e1ef --- /dev/null +++ b/pathod/language/writer.py @@ -0,0 +1,67 @@ +import time +from netlib.exceptions import TcpDisconnect +import netlib.tcp + +BLOCKSIZE = 1024 +# It's not clear what the upper limit for time.sleep is. It's lower than the +# maximum int or float. 1 year should do. +FOREVER = 60 * 60 * 24 * 365 + + +def send_chunk(fp, val, blocksize, start, end): + """ + (start, end): Inclusive lower bound, exclusive upper bound. + """ + for i in range(start, end, blocksize): + fp.write( + val[i:min(i + blocksize, end)] + ) + return end - start + + +def write_values(fp, vals, actions, sofar=0, blocksize=BLOCKSIZE): + """ + vals: A list of values, which may be strings or Value objects. + + actions: A list of (offset, action, arg) tuples. Action may be "pause" + or "disconnect". + + Both vals and actions are in reverse order, with the first items last. + + Return True if connection should disconnect. + """ + sofar = 0 + try: + while vals: + v = vals.pop() + offset = 0 + while actions and actions[-1][0] < (sofar + len(v)): + a = actions.pop() + offset += send_chunk( + fp, + v, + blocksize, + offset, + a[0] - sofar - offset + ) + if a[1] == "pause": + time.sleep( + FOREVER if a[2] == "f" else a[2] + ) + elif a[1] == "disconnect": + return True + elif a[1] == "inject": + send_chunk(fp, a[2], blocksize, 0, len(a[2])) + send_chunk(fp, v, blocksize, offset, len(v)) + sofar += len(v) + # Remainders + while actions: + a = actions.pop() + if a[1] == "pause": + time.sleep(a[2]) + elif a[1] == "disconnect": + return True + elif a[1] == "inject": + send_chunk(fp, a[2], blocksize, 0, len(a[2])) + except TcpDisconnect: # pragma: no cover + return True diff --git a/pathod/log.py b/pathod/log.py new file mode 100644 index 00000000..f203542f --- /dev/null +++ b/pathod/log.py @@ -0,0 +1,83 @@ +import datetime + +import netlib.utils +import netlib.tcp +import netlib.http + +TIMEFMT = '%d-%m-%y %H:%M:%S' + + +def write_raw(fp, lines): + if fp: + fp.write( + "%s: " % datetime.datetime.now().strftime(TIMEFMT) + ) + for i in lines: + fp.write(i) + fp.write("\n") + fp.flush() + + +class LogCtx(object): + + def __init__(self, fp, hex, rfile, wfile): + self.lines = [] + self.fp = fp + self.suppressed = False + self.hex = hex + self.rfile, self.wfile = rfile, wfile + + def __enter__(self): + if self.wfile: + self.wfile.start_log() + if self.rfile: + self.rfile.start_log() + return self + + def __exit__(self, exc_type, exc_value, traceback): + wlog = self.wfile.get_log() if self.wfile else None + rlog = self.rfile.get_log() if self.rfile else None + if self.suppressed or not self.fp: + return + if wlog: + self("Bytes written:") + self.dump(wlog, self.hex) + if rlog: + self("Bytes read:") + self.dump(rlog, self.hex) + if self.lines: + write_raw( + self.fp, + [ + "\n".join(self.lines), + ] + ) + if exc_value: + raise exc_type, exc_value, traceback + + def suppress(self): + self.suppressed = True + + def dump(self, data, hexdump): + if hexdump: + for line in netlib.utils.hexdump(data): + self("\t%s %s %s" % line) + else: + for i in netlib.utils.clean_bin(data).split("\n"): + self("\t%s" % i) + + def __call__(self, line): + self.lines.append(line) + + +class ConnectionLogger: + def __init__(self, fp, hex, rfile, wfile): + self.fp = fp + self.hex = hex + self.rfile, self.wfile = rfile, wfile + + def ctx(self): + return LogCtx(self.fp, self.hex, self.rfile, self.wfile) + + def write(self, lines): + write_raw(self.fp, lines) diff --git a/pathod/pathoc.py b/pathod/pathoc.py new file mode 100644 index 00000000..c0a33b62 --- /dev/null +++ b/pathod/pathoc.py @@ -0,0 +1,534 @@ +import contextlib +import sys +import os +import itertools +import hashlib +import Queue +import random +import select +import time +import threading + +import OpenSSL.crypto +import six + +from netlib import tcp, http, certutils, websockets, socks +from netlib.exceptions import HttpException, TcpDisconnect, TcpTimeout, TlsException, TcpException, \ + NetlibException +from netlib.http import http1, http2 + +import language.http +import language.websockets +from . import utils, log + +import logging +from netlib.tutils import treq + +logging.getLogger("hpack").setLevel(logging.WARNING) + + +class PathocError(Exception): + pass + + +class SSLInfo(object): + + def __init__(self, certchain, cipher, alp): + self.certchain, self.cipher, self.alp = certchain, cipher, alp + + def __str__(self): + parts = [ + "Application Layer Protocol: %s" % self.alp, + "Cipher: %s, %s bit, %s" % self.cipher, + "SSL certificate chain:" + ] + for i in self.certchain: + parts.append("\tSubject: ") + for cn in i.get_subject().get_components(): + parts.append("\t\t%s=%s" % cn) + parts.append("\tIssuer: ") + for cn in i.get_issuer().get_components(): + parts.append("\t\t%s=%s" % cn) + parts.extend( + [ + "\tVersion: %s" % i.get_version(), + "\tValidity: %s - %s" % ( + i.get_notBefore(), i.get_notAfter() + ), + "\tSerial: %s" % i.get_serial_number(), + "\tAlgorithm: %s" % i.get_signature_algorithm() + ] + ) + pk = i.get_pubkey() + types = { + OpenSSL.crypto.TYPE_RSA: "RSA", + OpenSSL.crypto.TYPE_DSA: "DSA" + } + t = types.get(pk.type(), "Uknown") + parts.append("\tPubkey: %s bit %s" % (pk.bits(), t)) + s = certutils.SSLCert(i) + if s.altnames: + parts.append("\tSANs: %s" % " ".join(s.altnames)) + return "\n".join(parts) + + + +class WebsocketFrameReader(threading.Thread): + + def __init__( + self, + rfile, + logfp, + showresp, + hexdump, + ws_read_limit, + timeout + ): + threading.Thread.__init__(self) + self.timeout = timeout + self.ws_read_limit = ws_read_limit + self.logfp = logfp + self.showresp = showresp + self.hexdump = hexdump + self.rfile = rfile + self.terminate = Queue.Queue() + self.frames_queue = Queue.Queue() + self.logger = log.ConnectionLogger( + self.logfp, + self.hexdump, + rfile if showresp else None, + None + ) + + @contextlib.contextmanager + def terminator(self): + yield + self.frames_queue.put(None) + + def run(self): + starttime = time.time() + with self.terminator(): + while True: + if self.ws_read_limit == 0: + return + r, _, _ = select.select([self.rfile], [], [], 0.05) + delta = time.time() - starttime + if not r and self.timeout and delta > self.timeout: + return + try: + self.terminate.get_nowait() + return + except Queue.Empty: + pass + for rfile in r: + with self.logger.ctx() as log: + try: + frm = websockets.Frame.from_file(self.rfile) + except TcpDisconnect: + return + self.frames_queue.put(frm) + log("<< %s" % frm.header.human_readable()) + if self.ws_read_limit is not None: + self.ws_read_limit -= 1 + starttime = time.time() + + +class Pathoc(tcp.TCPClient): + + def __init__( + self, + address, + + # SSL + ssl=None, + sni=None, + ssl_version=tcp.SSL_DEFAULT_METHOD, + ssl_options=tcp.SSL_DEFAULT_OPTIONS, + clientcert=None, + ciphers=None, + + # HTTP/2 + use_http2=False, + http2_skip_connection_preface=False, + http2_framedump=False, + + # Websockets + ws_read_limit=None, + + # Network + timeout=None, + + # Output control + showreq=False, + showresp=False, + explain=False, + hexdump=False, + ignorecodes=(), + ignoretimeout=False, + showsummary=False, + fp=sys.stdout + ): + """ + spec: A request specification + showreq: Print requests + showresp: Print responses + explain: Print request explanation + showssl: Print info on SSL connection + hexdump: When printing requests or responses, use hex dump output + showsummary: Show a summary of requests + ignorecodes: Sequence of return codes to ignore + """ + tcp.TCPClient.__init__(self, address) + + self.ssl, self.sni = ssl, sni + self.clientcert = clientcert + self.ssl_version = ssl_version + self.ssl_options = ssl_options + self.ciphers = ciphers + self.sslinfo = None + + self.use_http2 = use_http2 + self.http2_skip_connection_preface = http2_skip_connection_preface + self.http2_framedump = http2_framedump + + self.ws_read_limit = ws_read_limit + + self.timeout = timeout + + self.showreq = showreq + self.showresp = showresp + self.explain = explain + self.hexdump = hexdump + self.ignorecodes = ignorecodes + self.ignoretimeout = ignoretimeout + self.showsummary = showsummary + self.fp = fp + + self.ws_framereader = None + + if self.use_http2: + if not tcp.HAS_ALPN: # pragma: nocover + log.write_raw( + self.fp, + "HTTP/2 requires ALPN support. " + "Please use OpenSSL >= 1.0.2. " + "Pathoc might not be working as expected without ALPN." + ) + self.protocol = http2.HTTP2Protocol(self, dump_frames=self.http2_framedump) + else: + self.protocol = http1 + + self.settings = language.Settings( + is_client=True, + staticdir=os.getcwd(), + unconstrained_file_access=True, + request_host=self.address.host, + protocol=self.protocol, + ) + + def http_connect(self, connect_to): + self.wfile.write( + 'CONNECT %s:%s HTTP/1.1\r\n' % tuple(connect_to) + + '\r\n' + ) + self.wfile.flush() + try: + resp = self.protocol.read_response(self.rfile, treq(method="CONNECT")) + if resp.status_code != 200: + raise HttpException("Unexpected status code: %s" % resp.status_code) + except HttpException as e: + six.reraise(PathocError, PathocError( + "Proxy CONNECT failed: %s" % repr(e) + )) + + def socks_connect(self, connect_to): + try: + client_greet = socks.ClientGreeting(socks.VERSION.SOCKS5, [socks.METHOD.NO_AUTHENTICATION_REQUIRED]) + client_greet.to_file(self.wfile) + self.wfile.flush() + + server_greet = socks.ServerGreeting.from_file(self.rfile) + server_greet.assert_socks5() + if server_greet.method != socks.METHOD.NO_AUTHENTICATION_REQUIRED: + raise socks.SocksError( + socks.METHOD.NO_ACCEPTABLE_METHODS, + "pathoc only supports SOCKS without authentication" + ) + + connect_request = socks.Message( + socks.VERSION.SOCKS5, + socks.CMD.CONNECT, + socks.ATYP.DOMAINNAME, + tcp.Address.wrap(connect_to) + ) + connect_request.to_file(self.wfile) + self.wfile.flush() + + connect_reply = socks.Message.from_file(self.rfile) + connect_reply.assert_socks5() + if connect_reply.msg != socks.REP.SUCCEEDED: + raise socks.SocksError( + connect_reply.msg, + "SOCKS server error" + ) + except (socks.SocksError, TcpDisconnect) as e: + raise PathocError(str(e)) + + def connect(self, connect_to=None, showssl=False, fp=sys.stdout): + """ + connect_to: A (host, port) tuple, which will be connected to with + an HTTP CONNECT request. + """ + if self.use_http2 and not self.ssl: + raise NotImplementedError("HTTP2 without SSL is not supported.") + + tcp.TCPClient.connect(self) + + if connect_to: + self.http_connect(connect_to) + + self.sslinfo = None + if self.ssl: + try: + alpn_protos = [b'http/1.1'] + if self.use_http2: + alpn_protos.append(b'h2') + + self.convert_to_ssl( + sni=self.sni, + cert=self.clientcert, + method=self.ssl_version, + options=self.ssl_options, + cipher_list=self.ciphers, + alpn_protos=alpn_protos + ) + except TlsException as v: + raise PathocError(str(v)) + + self.sslinfo = SSLInfo( + self.connection.get_peer_cert_chain(), + self.get_current_cipher(), + self.get_alpn_proto_negotiated() + ) + if showssl: + print >> fp, str(self.sslinfo) + + if self.use_http2: + self.protocol.check_alpn() + if not self.http2_skip_connection_preface: + self.protocol.perform_client_connection_preface() + + if self.timeout: + self.settimeout(self.timeout) + + def stop(self): + if self.ws_framereader: + self.ws_framereader.terminate.put(None) + + def wait(self, timeout=0.01, finish=True): + """ + A generator that yields frames until Pathoc terminates. + + timeout: If specified None may be yielded instead if timeout is + reached. If timeout is None, wait forever. If timeout is 0, return + immedately if nothing is on the queue. + + finish: If true, consume messages until the reader shuts down. + Otherwise, return None on timeout. + """ + if self.ws_framereader: + while True: + try: + frm = self.ws_framereader.frames_queue.get( + timeout=timeout, + block=True if timeout != 0 else False + ) + except Queue.Empty: + if finish: + continue + else: + return + if frm is None: + self.ws_framereader.join() + return + yield frm + + def websocket_send_frame(self, r): + """ + Sends a single websocket frame. + """ + logger = log.ConnectionLogger( + self.fp, + self.hexdump, + None, + self.wfile if self.showreq else None, + ) + with logger.ctx() as lg: + lg(">> %s" % r) + language.serve(r, self.wfile, self.settings) + self.wfile.flush() + + def websocket_start(self, r): + """ + Performs an HTTP request, and attempts to drop into websocket + connection. + """ + resp = self.http(r) + if resp.status_code == 101: + self.ws_framereader = WebsocketFrameReader( + self.rfile, + self.fp, + self.showresp, + self.hexdump, + self.ws_read_limit, + self.timeout + ) + self.ws_framereader.start() + return resp + + def http(self, r): + """ + Performs a single request. + + r: A language.http.Request object, or a string representing one + request. + + Returns Response if we have a non-ignored response. + + May raise a NetlibException + """ + logger = log.ConnectionLogger( + self.fp, + self.hexdump, + self.rfile if self.showresp else None, + self.wfile if self.showreq else None, + ) + with logger.ctx() as lg: + lg(">> %s" % r) + resp, req = None, None + try: + req = language.serve(r, self.wfile, self.settings) + self.wfile.flush() + + resp = self.protocol.read_response(self.rfile, treq(method=req["method"].encode())) + resp.sslinfo = self.sslinfo + except HttpException as v: + lg("Invalid server response: %s" % v) + raise + except TcpTimeout: + if self.ignoretimeout: + lg("Timeout (ignored)") + return None + lg("Timeout") + raise + finally: + if resp: + lg("<< %s %s: %s bytes" % ( + resp.status_code, utils.xrepr(resp.msg), len(resp.content) + )) + if resp.status_code in self.ignorecodes: + lg.suppress() + return resp + + def request(self, r): + """ + Performs a single request. + + r: A language.message.Messsage object, or a string representing + one. + + Returns Response if we have a non-ignored response. + + May raise a NetlibException + """ + if isinstance(r, basestring): + r = language.parse_pathoc(r, self.use_http2).next() + + if isinstance(r, language.http.Request): + if r.ws: + return self.websocket_start(r) + else: + return self.http(r) + elif isinstance(r, language.websockets.WebsocketFrame): + self.websocket_send_frame(r) + elif isinstance(r, language.http2.Request): + return self.http(r) + # elif isinstance(r, language.http2.Frame): + # TODO: do something + + +def main(args): # pragma: nocover + memo = set([]) + trycount = 0 + p = None + try: + cnt = 0 + while True: + if cnt == args.repeat and args.repeat != 0: + break + if args.wait and cnt != 0: + time.sleep(args.wait) + + cnt += 1 + playlist = itertools.chain(*args.requests) + if args.random: + playlist = random.choice(args.requests) + p = Pathoc( + (args.host, args.port), + ssl=args.ssl, + sni=args.sni, + ssl_version=args.ssl_version, + ssl_options=args.ssl_options, + clientcert=args.clientcert, + ciphers=args.ciphers, + use_http2=args.use_http2, + http2_skip_connection_preface=args.http2_skip_connection_preface, + http2_framedump=args.http2_framedump, + showreq=args.showreq, + showresp=args.showresp, + explain=args.explain, + hexdump=args.hexdump, + ignorecodes=args.ignorecodes, + timeout=args.timeout, + ignoretimeout=args.ignoretimeout, + showsummary=True + ) + trycount = 0 + try: + p.connect(args.connect_to, args.showssl) + except TcpException as v: + print >> sys.stderr, str(v) + continue + except PathocError as v: + print >> sys.stderr, str(v) + sys.exit(1) + for spec in playlist: + if args.explain or args.memo: + spec = spec.freeze(p.settings) + if args.memo: + h = hashlib.sha256(spec.spec()).digest() + if h not in memo: + trycount = 0 + memo.add(h) + else: + trycount += 1 + if trycount > args.memolimit: + print >> sys.stderr, "Memo limit exceeded..." + return + else: + continue + try: + ret = p.request(spec) + if ret and args.oneshot: + return + # We consume the queue when we can, so it doesn't build up. + for i_ in p.wait(timeout=0, finish=False): + pass + except NetlibException: + break + for i_ in p.wait(timeout=0.01, finish=True): + pass + except KeyboardInterrupt: + pass + if p: + p.stop() diff --git a/pathod/pathoc_cmdline.py b/pathod/pathoc_cmdline.py new file mode 100644 index 00000000..bf827a9a --- /dev/null +++ b/pathod/pathoc_cmdline.py @@ -0,0 +1,226 @@ +import sys +import argparse +import os +import os.path + +from netlib import tcp +from netlib.http import user_agents +from . import pathoc, version, language + + +def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr): + preparser = argparse.ArgumentParser(add_help=False) + preparser.add_argument( + "--show-uas", dest="showua", action="store_true", default=False, + help="Print user agent shortcuts and exit." + ) + pa = preparser.parse_known_args(argv)[0] + if pa.showua: + print >> stdout, "User agent strings:" + for i in user_agents.UASTRINGS: + print >> stdout, " ", i[1], i[0] + sys.exit(0) + + parser = argparse.ArgumentParser( + description='A perverse HTTP client.', parents=[preparser] + ) + parser.add_argument( + '--version', + action='version', + version="pathoc " + version.VERSION + ) + parser.add_argument( + "-c", dest="connect_to", type=str, default=False, + metavar="HOST:PORT", + help="Issue an HTTP CONNECT to connect to the specified host." + ) + parser.add_argument( + "--memo-limit", dest='memolimit', default=5000, type=int, metavar="N", + help='Stop if we do not find a valid request after N attempts.' + ) + parser.add_argument( + "-m", dest='memo', action="store_true", default=False, + help=""" + Remember specs, and never play the same one twice. Note that this + means requests have to be rendered in memory, which means that + large generated data can cause issues. + """ + ) + parser.add_argument( + "-n", dest='repeat', default=1, type=int, metavar="N", + help='Repeat N times. If 0 repeat for ever.' + ) + parser.add_argument( + "-w", dest='wait', default=0, type=float, metavar="N", + help='Wait N seconds between each request.' + ) + parser.add_argument( + "-r", dest="random", action="store_true", default=False, + help=""" + Select a random request from those specified. If this is not specified, + requests are all played in sequence. + """ + ) + parser.add_argument( + "-t", dest="timeout", type=int, default=None, + help="Connection timeout" + ) + parser.add_argument( + "--http2", dest="use_http2", action="store_true", default=False, + help='Perform all requests over a single HTTP/2 connection.' + ) + parser.add_argument( + "--http2-skip-connection-preface", + dest="http2_skip_connection_preface", + action="store_true", + default=False, + help='Skips the HTTP/2 connection preface before sending requests.') + + parser.add_argument( + 'host', type=str, + metavar="host[:port]", + help='Host and port to connect to' + ) + parser.add_argument( + 'requests', type=str, nargs="+", + help=""" + Request specification, or path to a file containing request + specifcations + """ + ) + + group = parser.add_argument_group( + 'SSL', + ) + group.add_argument( + "-s", dest="ssl", action="store_true", default=False, + help="Connect with SSL" + ) + group.add_argument( + "-C", dest="clientcert", type=str, default=False, + help="Path to a file containing client certificate and private key" + ) + group.add_argument( + "-i", dest="sni", type=str, default=False, + help="SSL Server Name Indication" + ) + group.add_argument( + "--ciphers", dest="ciphers", type=str, default=False, + help="SSL cipher specification" + ) + group.add_argument( + "--ssl-version", dest="ssl_version", type=str, default="secure", + choices=tcp.sslversion_choices.keys(), + help="Set supported SSL/TLS versions. " + "SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+." + ) + + group = parser.add_argument_group( + 'Controlling Output', + """ + Some of these options expand generated values for logging - if + you're generating large data, use them with caution. + """ + ) + group.add_argument( + "-I", dest="ignorecodes", type=str, default="", + help="Comma-separated list of response codes to ignore" + ) + group.add_argument( + "-S", dest="showssl", action="store_true", default=False, + help="Show info on SSL connection" + ) + group.add_argument( + "-e", dest="explain", action="store_true", default=False, + help="Explain requests" + ) + group.add_argument( + "-o", dest="oneshot", action="store_true", default=False, + help="Oneshot - exit after first non-ignored response" + ) + group.add_argument( + "-q", dest="showreq", action="store_true", default=False, + help="Print full request" + ) + group.add_argument( + "-p", dest="showresp", action="store_true", default=False, + help="Print full response" + ) + group.add_argument( + "-T", dest="ignoretimeout", action="store_true", default=False, + help="Ignore timeouts" + ) + group.add_argument( + "-x", dest="hexdump", action="store_true", default=False, + help="Output in hexdump format" + ) + group.add_argument( + "--http2-framedump", dest="http2_framedump", action="store_true", default=False, + help="Output all received & sent HTTP/2 frames" + ) + + args = parser.parse_args(argv[1:]) + + args.ssl_version, args.ssl_options = tcp.sslversion_choices[args.ssl_version] + + args.port = None + if ":" in args.host: + h, p = args.host.rsplit(":", 1) + try: + p = int(p) + except ValueError: + return parser.error("Invalid port in host spec: %s" % args.host) + args.host = h + args.port = p + + if args.port is None: + args.port = 443 if args.ssl else 80 + + try: + args.ignorecodes = [int(i) for i in args.ignorecodes.split(",") if i] + except ValueError: + return parser.error( + "Invalid return code specification: %s" % + args.ignorecodes) + + if args.connect_to: + parts = args.connect_to.split(":") + if len(parts) != 2: + return parser.error( + "Invalid CONNECT specification: %s" % + args.connect_to) + try: + parts[1] = int(parts[1]) + except ValueError: + return parser.error( + "Invalid CONNECT specification: %s" % + args.connect_to) + args.connect_to = parts + else: + args.connect_to = None + + if args.http2_skip_connection_preface: + args.use_http2 = True + + if args.use_http2: + args.ssl = True + + reqs = [] + for r in args.requests: + if os.path.isfile(r): + data = open(r).read() + r = data + try: + reqs.append(language.parse_pathoc(r, args.use_http2)) + except language.ParseException as v: + print >> stderr, "Error parsing request spec: %s" % v.msg + print >> stderr, v.marked() + sys.exit(1) + args.requests = reqs + + return args + + +def go_pathoc(): # pragma: nocover + args = args_pathoc(sys.argv) + pathoc.main(args) diff --git a/pathod/pathod.py b/pathod/pathod.py new file mode 100644 index 00000000..55e75074 --- /dev/null +++ b/pathod/pathod.py @@ -0,0 +1,503 @@ +import copy +import logging +import os +import sys +import threading +import urllib + +from netlib import tcp, http, certutils, websockets +from netlib.exceptions import HttpException, HttpReadDisconnect, TcpTimeout, TcpDisconnect, \ + TlsException + +from . import version, app, language, utils, log, protocols +import language.http +import language.actions +import language.exceptions +import language.websockets + + +DEFAULT_CERT_DOMAIN = "pathod.net" +CONFDIR = "~/.mitmproxy" +CERTSTORE_BASENAME = "mitmproxy" +CA_CERT_NAME = "mitmproxy-ca.pem" +DEFAULT_CRAFT_ANCHOR = "/p/" + +logger = logging.getLogger('pathod') + + +class PathodError(Exception): + pass + + +class SSLOptions(object): + def __init__( + self, + confdir=CONFDIR, + cn=None, + sans=(), + not_after_connect=None, + request_client_cert=False, + ssl_version=tcp.SSL_DEFAULT_METHOD, + ssl_options=tcp.SSL_DEFAULT_OPTIONS, + ciphers=None, + certs=None, + alpn_select=b'h2', + ): + self.confdir = confdir + self.cn = cn + self.sans = sans + self.not_after_connect = not_after_connect + self.request_client_cert = request_client_cert + self.ssl_version = ssl_version + self.ssl_options = ssl_options + self.ciphers = ciphers + self.alpn_select = alpn_select + self.certstore = certutils.CertStore.from_store( + os.path.expanduser(confdir), + CERTSTORE_BASENAME + ) + for i in certs or []: + self.certstore.add_cert_file(*i) + + def get_cert(self, name): + if self.cn: + name = self.cn + elif not name: + name = DEFAULT_CERT_DOMAIN + return self.certstore.get_cert(name, self.sans) + + +class PathodHandler(tcp.BaseHandler): + wbufsize = 0 + sni = None + + def __init__( + self, + connection, + address, + server, + logfp, + settings, + http2_framedump=False + ): + tcp.BaseHandler.__init__(self, connection, address, server) + self.logfp = logfp + self.settings = copy.copy(settings) + self.protocol = None + self.use_http2 = False + self.http2_framedump = http2_framedump + + def handle_sni(self, connection): + self.sni = connection.get_servername() + + def http_serve_crafted(self, crafted, logctx): + error, crafted = self.server.check_policy( + crafted, self.settings + ) + if error: + err = self.make_http_error_response(error) + language.serve(err, self.wfile, self.settings) + return None, dict( + type="error", + msg=error + ) + + if self.server.explain and not hasattr(crafted, 'is_error_response'): + crafted = crafted.freeze(self.settings) + logctx(">> Spec: %s" % crafted.spec()) + + response_log = language.serve( + crafted, + self.wfile, + self.settings + ) + if response_log["disconnect"]: + return None, response_log + return self.handle_http_request, response_log + + + def handle_http_request(self, logger): + """ + Returns a (handler, log) tuple. + + handler: Handler for the next request, or None to disconnect + log: A dictionary, or None + """ + with logger.ctx() as lg: + try: + req = self.protocol.read_request(self.rfile) + except HttpReadDisconnect: + return None, None + except HttpException as s: + s = str(s) + lg(s) + return None, dict(type="error", msg=s) + + if req.method == 'CONNECT': + return self.protocol.handle_http_connect([req.host, req.port, req.http_version], lg) + + method = req.method + path = req.path + http_version = req.http_version + headers = req.headers + body = req.content + + clientcert = None + if self.clientcert: + clientcert = dict( + cn=self.clientcert.cn, + subject=self.clientcert.subject, + serial=self.clientcert.serial, + notbefore=self.clientcert.notbefore.isoformat(), + notafter=self.clientcert.notafter.isoformat(), + keyinfo=self.clientcert.keyinfo, + ) + + retlog = dict( + type="crafted", + protocol="http", + request=dict( + path=path, + method=method, + headers=headers.fields, + http_version=http_version, + sni=self.sni, + remote_address=self.address(), + clientcert=clientcert, + ), + cipher=None, + ) + if self.ssl_established: + retlog["cipher"] = self.get_current_cipher() + + m = utils.MemBool() + websocket_key = websockets.WebsocketsProtocol.check_client_handshake(headers) + self.settings.websocket_key = websocket_key + + # If this is a websocket initiation, we respond with a proper + # server response, unless over-ridden. + if websocket_key: + anchor_gen = language.parse_pathod("ws") + else: + anchor_gen = None + + for regex, spec in self.server.anchors: + if regex.match(path): + anchor_gen = language.parse_pathod(spec, self.use_http2) + break + else: + if m(path.startswith(self.server.craftanchor)): + spec = urllib.unquote(path)[len(self.server.craftanchor):] + if spec: + try: + anchor_gen = language.parse_pathod(spec, self.use_http2) + except language.ParseException as v: + lg("Parse error: %s" % v.msg) + anchor_gen = iter([self.make_http_error_response( + "Parse Error", + "Error parsing response spec: %s\n" % ( + v.msg + v.marked() + ) + )]) + else: + if self.use_http2: + anchor_gen = iter([self.make_http_error_response( + "Spec Error", + "HTTP/2 only supports request/response with the craft anchor point: %s" % + self.server.craftanchor + )]) + + if anchor_gen: + spec = anchor_gen.next() + + if self.use_http2 and isinstance(spec, language.http2.Response): + spec.stream_id = req.stream_id + + lg("crafting spec: %s" % spec) + nexthandler, retlog["response"] = self.http_serve_crafted( + spec, + lg + ) + if nexthandler and websocket_key: + self.protocol = protocols.websockets.WebsocketsProtocol(self) + return self.protocol.handle_websocket, retlog + else: + return nexthandler, retlog + else: + return self.protocol.handle_http_app(method, path, headers, body, lg) + + def make_http_error_response(self, reason, body=None): + resp = self.protocol.make_error_response(reason, body) + resp.is_error_response = True + return resp + + def handle(self): + self.settimeout(self.server.timeout) + + if self.server.ssl: + try: + cert, key, _ = self.server.ssloptions.get_cert(None) + self.convert_to_ssl( + cert, + key, + handle_sni=self.handle_sni, + request_client_cert=self.server.ssloptions.request_client_cert, + cipher_list=self.server.ssloptions.ciphers, + method=self.server.ssloptions.ssl_version, + options=self.server.ssloptions.ssl_options, + alpn_select=self.server.ssloptions.alpn_select, + ) + except TlsException as v: + s = str(v) + self.server.add_log( + dict( + type="error", + msg=s + ) + ) + log.write_raw(self.logfp, s) + return + + alp = self.get_alpn_proto_negotiated() + if alp == b'h2': + self.protocol = protocols.http2.HTTP2Protocol(self) + self.use_http2 = True + + if not self.protocol: + self.protocol = protocols.http.HTTPProtocol(self) + + lr = self.rfile if self.server.logreq else None + lw = self.wfile if self.server.logresp else None + logger = log.ConnectionLogger(self.logfp, self.server.hexdump, lr, lw) + + self.settings.protocol = self.protocol + + handler = self.handle_http_request + + while not self.finished: + handler, l = handler(logger) + if l: + self.addlog(l) + if not handler: + return + + def addlog(self, log): + # FIXME: The bytes in the log should not be escaped. We do this at the + # moment because JSON encoding can't handle binary data, and I don't + # want to base64 everything. + if self.server.logreq: + encoded_bytes = self.rfile.get_log().encode("string_escape") + log["request_bytes"] = encoded_bytes + if self.server.logresp: + encoded_bytes = self.wfile.get_log().encode("string_escape") + log["response_bytes"] = encoded_bytes + self.server.add_log(log) + + +class Pathod(tcp.TCPServer): + LOGBUF = 500 + + def __init__( + self, + addr, + ssl=False, + ssloptions=None, + craftanchor=DEFAULT_CRAFT_ANCHOR, + staticdir=None, + anchors=(), + sizelimit=None, + noweb=False, + nocraft=False, + noapi=False, + nohang=False, + timeout=None, + logreq=False, + logresp=False, + explain=False, + hexdump=False, + http2_framedump=False, + webdebug=False, + logfp=sys.stdout, + ): + """ + addr: (address, port) tuple. If port is 0, a free port will be + automatically chosen. + ssloptions: an SSLOptions object. + craftanchor: URL prefix specifying the path under which to anchor + response generation. + staticdir: path to a directory of static resources, or None. + anchors: List of (regex object, language.Request object) tuples, or + None. + sizelimit: Limit size of served data. + nocraft: Disable response crafting. + noapi: Disable the API. + nohang: Disable pauses. + """ + tcp.TCPServer.__init__(self, addr) + self.ssl = ssl + self.ssloptions = ssloptions or SSLOptions() + self.staticdir = staticdir + self.craftanchor = craftanchor + self.sizelimit = sizelimit + self.noweb, self.nocraft = noweb, nocraft + self.noapi, self.nohang = noapi, nohang + self.timeout, self.logreq = timeout, logreq + self.logresp, self.hexdump = logresp, hexdump + self.http2_framedump = http2_framedump + self.explain = explain + self.logfp = logfp + + self.app = app.make_app(noapi, webdebug) + self.app.config["pathod"] = self + self.log = [] + self.logid = 0 + self.anchors = anchors + + self.settings = language.Settings( + staticdir=self.staticdir + ) + + def check_policy(self, req, settings): + """ + A policy check that verifies the request size is within limits. + """ + if self.nocraft: + return "Crafting disabled.", None + try: + req = req.resolve(settings) + l = req.maximum_length(settings) + except language.FileAccessDenied: + return "File access denied.", None + if self.sizelimit and l > self.sizelimit: + return "Response too large.", None + pauses = [isinstance(i, language.actions.PauseAt) for i in req.actions] + if self.nohang and any(pauses): + return "Pauses have been disabled.", None + return None, req + + def handle_client_connection(self, request, client_address): + h = PathodHandler( + request, + client_address, + self, + self.logfp, + self.settings, + self.http2_framedump, + ) + try: + h.handle() + h.finish() + except TcpDisconnect: # pragma: no cover + log.write_raw(self.logfp, "Disconnect") + self.add_log( + dict( + type="error", + msg="Disconnect" + ) + ) + return + except TcpTimeout: + log.write_raw(self.logfp, "Timeout") + self.add_log( + dict( + type="timeout", + ) + ) + return + + def add_log(self, d): + if not self.noapi: + lock = threading.Lock() + with lock: + d["id"] = self.logid + self.log.insert(0, d) + if len(self.log) > self.LOGBUF: + self.log.pop() + self.logid += 1 + return d["id"] + + def clear_log(self): + lock = threading.Lock() + with lock: + self.log = [] + + def log_by_id(self, identifier): + for i in self.log: + if i["id"] == identifier: + return i + + def get_log(self): + return self.log + + +def main(args): # pragma: nocover + ssloptions = SSLOptions( + cn=args.cn, + confdir=args.confdir, + not_after_connect=args.ssl_not_after_connect, + ciphers=args.ciphers, + ssl_version=args.ssl_version, + ssl_options=args.ssl_options, + certs=args.ssl_certs, + sans=args.sans, + ) + + root = logging.getLogger() + if root.handlers: + for handler in root.handlers: + root.removeHandler(handler) + + log = logging.getLogger('pathod') + log.setLevel(logging.DEBUG) + fmt = logging.Formatter( + '%(asctime)s: %(message)s', + datefmt='%d-%m-%y %H:%M:%S', + ) + if args.logfile: + fh = logging.handlers.WatchedFileHandler(args.logfile) + fh.setFormatter(fmt) + log.addHandler(fh) + if not args.daemonize: + sh = logging.StreamHandler() + sh.setFormatter(fmt) + log.addHandler(sh) + + try: + pd = Pathod( + (args.address, args.port), + craftanchor=args.craftanchor, + ssl=args.ssl, + ssloptions=ssloptions, + staticdir=args.staticdir, + anchors=args.anchors, + sizelimit=args.sizelimit, + noweb=args.noweb, + nocraft=args.nocraft, + noapi=args.noapi, + nohang=args.nohang, + timeout=args.timeout, + logreq=args.logreq, + logresp=args.logresp, + hexdump=args.hexdump, + http2_framedump=args.http2_framedump, + explain=args.explain, + webdebug=args.webdebug + ) + except PathodError as v: + print >> sys.stderr, "Error: %s" % v + sys.exit(1) + except language.FileAccessDenied as v: + print >> sys.stderr, "Error: %s" % v + + if args.daemonize: + utils.daemonize() + + try: + print "%s listening on %s:%s" % ( + version.NAMEVERSION, + pd.address.host, + pd.address.port + ) + pd.serve_forever() + except KeyboardInterrupt: + pass diff --git a/pathod/pathod/__init__.py b/pathod/pathod/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pathod/pathod/app.py b/pathod/pathod/app.py deleted file mode 100644 index c6b7eabc..00000000 --- a/pathod/pathod/app.py +++ /dev/null @@ -1,179 +0,0 @@ -import logging -import pprint -import cStringIO -import copy -from flask import Flask, jsonify, render_template, request, abort, make_response -from . import version, language, utils -from netlib.http import user_agents - -logging.basicConfig(level="DEBUG") -EXAMPLE_HOST = "example.com" -EXAMPLE_WEBSOCKET_KEY = "examplekey" - -# pylint: disable=unused-variable - - -def make_app(noapi, debug): - app = Flask(__name__) - app.debug = debug - - if not noapi: - @app.route('/api/info') - def api_info(): - return jsonify( - version=version.IVERSION - ) - - @app.route('/api/log') - def api_log(): - return jsonify( - log=app.config["pathod"].get_log() - ) - - @app.route('/api/clear_log') - def api_clear_log(): - app.config["pathod"].clear_log() - return "OK" - - def render(s, cacheable, **kwargs): - kwargs["noapi"] = app.config["pathod"].noapi - kwargs["nocraft"] = app.config["pathod"].nocraft - kwargs["craftanchor"] = app.config["pathod"].craftanchor - resp = make_response(render_template(s, **kwargs), 200) - if cacheable: - resp.headers["Cache-control"] = "public, max-age=4320" - return resp - - @app.route('/') - @app.route('/index.html') - def index(): - return render( - "index.html", - True, - section="main", - version=version.VERSION - ) - - @app.route('/download') - @app.route('/download.html') - def download(): - return render( - "download.html", True, section="download", version=version.VERSION - ) - - @app.route('/about') - @app.route('/about.html') - def about(): - return render("about.html", True, section="about") - - @app.route('/docs/pathod') - def docs_pathod(): - return render( - "docs_pathod.html", True, section="docs", subsection="pathod" - ) - - @app.route('/docs/language') - def docs_language(): - return render( - "docs_lang.html", True, - section="docs", uastrings=user_agents.UASTRINGS, - subsection="lang" - ) - - @app.route('/docs/pathoc') - def docs_pathoc(): - return render( - "docs_pathoc.html", True, section="docs", subsection="pathoc" - ) - - @app.route('/docs/lib_pathod') - def docs_lib_pathod(): - return render( - "docs_lib_pathod.html", True, section="docs", subsection="pathod" - ) - - @app.route('/docs/test') - def docs_test(): - return render( - "docs_test.html", True, section="docs", subsection="test" - ) - - @app.route('/log') - def log(): - if app.config["pathod"].noapi: - abort(404) - return render( - "log.html", - False, - section="log", - log=app.config["pathod"].get_log() - ) - - @app.route('/log/') - def onelog(lid): - item = app.config["pathod"].log_by_id(int(lid)) - if not item: - abort(404) - l = pprint.pformat(item) - return render("onelog.html", False, section="log", alog=l, lid=lid) - - def _preview(is_request): - if is_request: - template = "request_preview.html" - else: - template = "response_preview.html" - - spec = request.args["spec"] - - args = dict( - spec=spec, - section="main", - syntaxerror=None, - error=None, - ) - if not spec.strip(): - args["error"] = "Can't parse an empty spec." - return render(template, False, **args) - - try: - if is_request: - r = language.parse_pathoc(spec).next() - else: - r = language.parse_pathod(spec).next() - except language.ParseException as v: - args["syntaxerror"] = str(v) - args["marked"] = v.marked() - return render(template, False, **args) - - s = cStringIO.StringIO() - - settings = copy.copy(app.config["pathod"].settings) - settings.request_host = EXAMPLE_HOST - settings.websocket_key = EXAMPLE_WEBSOCKET_KEY - - safe = r.preview_safe() - err, safe = app.config["pathod"].check_policy( - safe, - settings - ) - if err: - args["error"] = err - return render(template, False, **args) - if is_request: - settings.request_host = EXAMPLE_HOST - language.serve(safe, s, settings) - else: - settings.websocket_key = EXAMPLE_WEBSOCKET_KEY - language.serve(safe, s, settings) - - args["output"] = utils.escape_unprintables(s.getvalue()) - return render(template, False, **args) - - @app.route('/response_preview') - def response_preview(): - return _preview(False) - - @app.route('/request_preview') - def request_preview(): - return _preview(True) - return app diff --git a/pathod/pathod/language/__init__.py b/pathod/pathod/language/__init__.py deleted file mode 100644 index 32199e08..00000000 --- a/pathod/pathod/language/__init__.py +++ /dev/null @@ -1,113 +0,0 @@ -import itertools -import time - -import pyparsing as pp - -from . import http, http2, websockets, writer, exceptions - -from exceptions import * -from base import Settings -assert Settings # prevent pyflakes from messing with this - - -def expand(msg): - times = getattr(msg, "times", None) - if times: - for j_ in xrange(int(times.value)): - yield msg.strike_token("times") - else: - yield msg - - -def parse_pathod(s, use_http2=False): - """ - May raise ParseException - """ - try: - s = s.decode("ascii") - except UnicodeError: - raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0) - try: - if use_http2: - expressions = [ - # http2.Frame.expr(), - http2.Response.expr(), - ] - else: - expressions = [ - websockets.WebsocketFrame.expr(), - http.Response.expr(), - ] - reqs = pp.Or(expressions).parseString(s, parseAll=True) - except pp.ParseException as v: - raise exceptions.ParseException(v.msg, v.line, v.col) - return itertools.chain(*[expand(i) for i in reqs]) - - -def parse_pathoc(s, use_http2=False): - try: - s = s.decode("ascii") - except UnicodeError: - raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0) - try: - if use_http2: - expressions = [ - # http2.Frame.expr(), - http2.Request.expr(), - ] - else: - expressions = [ - websockets.WebsocketClientFrame.expr(), - http.Request.expr(), - ] - reqs = pp.OneOrMore(pp.Or(expressions)).parseString(s, parseAll=True) - except pp.ParseException as v: - raise exceptions.ParseException(v.msg, v.line, v.col) - return itertools.chain(*[expand(i) for i in reqs]) - - -def parse_websocket_frame(s): - """ - May raise ParseException - """ - try: - reqs = pp.OneOrMore( - websockets.WebsocketFrame.expr() - ).parseString( - s, - parseAll=True - ) - except pp.ParseException as v: - raise exceptions.ParseException(v.msg, v.line, v.col) - return itertools.chain(*[expand(i) for i in reqs]) - - -def serve(msg, fp, settings): - """ - fp: The file pointer to write to. - - request_host: If this a request, this is the connecting host. If - None, we assume it's a response. Used to decide what standard - modifications to make if raw is not set. - - Calling this function may modify the object. - """ - msg = msg.resolve(settings) - started = time.time() - - vals = msg.values(settings) - vals.reverse() - - actions = sorted(msg.actions[:]) - actions.reverse() - actions = [i.intermediate(settings) for i in actions] - - disconnect = writer.write_values(fp, vals, actions[:]) - duration = time.time() - started - ret = dict( - disconnect=disconnect, - started=started, - duration=duration, - ) - ret.update(msg.log(settings)) - return ret diff --git a/pathod/pathod/language/actions.py b/pathod/pathod/language/actions.py deleted file mode 100644 index 34a9bafb..00000000 --- a/pathod/pathod/language/actions.py +++ /dev/null @@ -1,126 +0,0 @@ -import abc -import copy -import random - -import pyparsing as pp - -from . import base - - -class _Action(base.Token): - - """ - An action that operates on the raw data stream of the message. All - actions have one thing in common: an offset that specifies where the - action should take place. - """ - - def __init__(self, offset): - self.offset = offset - - def resolve(self, settings, msg): - """ - Resolves offset specifications to a numeric offset. Returns a copy - of the action object. - """ - c = copy.copy(self) - l = msg.length(settings) - if c.offset == "r": - c.offset = random.randrange(l) - elif c.offset == "a": - c.offset = l + 1 - return c - - def __cmp__(self, other): - return cmp(self.offset, other.offset) - - def __repr__(self): - return self.spec() - - @abc.abstractmethod - def spec(self): # pragma: no cover - pass - - @abc.abstractmethod - def intermediate(self, settings): # pragma: no cover - pass - - -class PauseAt(_Action): - unique_name = None - - def __init__(self, offset, seconds): - _Action.__init__(self, offset) - self.seconds = seconds - - @classmethod - def expr(cls): - e = pp.Literal("p").suppress() - e += base.TokOffset - e += pp.Literal(",").suppress() - e += pp.MatchFirst( - [ - base.v_integer, - pp.Literal("f") - ] - ) - return e.setParseAction(lambda x: cls(*x)) - - def spec(self): - return "p%s,%s" % (self.offset, self.seconds) - - def intermediate(self, settings): - return (self.offset, "pause", self.seconds) - - def freeze(self, settings_): - return self - - -class DisconnectAt(_Action): - - def __init__(self, offset): - _Action.__init__(self, offset) - - @classmethod - def expr(cls): - e = pp.Literal("d").suppress() - e += base.TokOffset - return e.setParseAction(lambda x: cls(*x)) - - def spec(self): - return "d%s" % self.offset - - def intermediate(self, settings): - return (self.offset, "disconnect") - - def freeze(self, settings_): - return self - - -class InjectAt(_Action): - unique_name = None - - def __init__(self, offset, value): - _Action.__init__(self, offset) - self.value = value - - @classmethod - def expr(cls): - e = pp.Literal("i").suppress() - e += base.TokOffset - e += pp.Literal(",").suppress() - e += base.TokValue - return e.setParseAction(lambda x: cls(*x)) - - def spec(self): - return "i%s,%s" % (self.offset, self.value.spec()) - - def intermediate(self, settings): - return ( - self.offset, - "inject", - self.value.get_generator(settings) - ) - - def freeze(self, settings): - return InjectAt(self.offset, self.value.freeze(settings)) diff --git a/pathod/pathod/language/base.py b/pathod/pathod/language/base.py deleted file mode 100644 index a4302998..00000000 --- a/pathod/pathod/language/base.py +++ /dev/null @@ -1,576 +0,0 @@ -import operator -import os -import abc -import pyparsing as pp - -from .. import utils -from . import generators, exceptions - -class Settings(object): - - def __init__( - self, - is_client=False, - staticdir=None, - unconstrained_file_access=False, - request_host=None, - websocket_key=None, - protocol=None, - ): - self.is_client = is_client - self.staticdir = staticdir - self.unconstrained_file_access = unconstrained_file_access - self.request_host = request_host - self.websocket_key = websocket_key # TODO: refactor this into the protocol - self.protocol = protocol - - -Sep = pp.Optional(pp.Literal(":")).suppress() - - -v_integer = pp.Word(pp.nums)\ - .setName("integer")\ - .setParseAction(lambda toks: int(toks[0])) - - -v_literal = pp.MatchFirst( - [ - pp.QuotedString( - "\"", - unquoteResults=True, - multiline=True - ), - pp.QuotedString( - "'", - unquoteResults=True, - multiline=True - ), - ] -) - -v_naked_literal = pp.MatchFirst( - [ - v_literal, - pp.Word("".join(i for i in pp.printables if i not in ",:\n@\'\"")) - ] -) - - -class Token(object): - - """ - A token in the specification language. Tokens are immutable. The token - classes have no meaning in and of themselves, and are combined into - Components and Actions to build the language. - """ - __metaclass__ = abc.ABCMeta - - @classmethod - def expr(cls): # pragma: no cover - """ - A parse expression. - """ - return None - - @abc.abstractmethod - def spec(self): # pragma: no cover - """ - A parseable specification for this token. - """ - return None - - @property - def unique_name(self): - """ - Controls uniqueness constraints for tokens. No two tokens with the - same name will be allowed. If no uniquness should be applied, this - should be None. - """ - return self.__class__.__name__.lower() - - def resolve(self, settings_, msg_): - """ - Resolves this token to ready it for transmission. This means that - the calculated offsets of actions are fixed. - - settings: a language.Settings instance - msg: The containing message - """ - return self - - def __repr__(self): - return self.spec() - - -class _TokValueLiteral(Token): - - def __init__(self, val): - self.val = val.decode("string_escape") - - def get_generator(self, settings_): - return self.val - - def freeze(self, settings_): - return self - - -class TokValueLiteral(_TokValueLiteral): - - """ - A literal with Python-style string escaping - """ - @classmethod - def expr(cls): - e = v_literal.copy() - return e.setParseAction(cls.parseAction) - - @classmethod - def parseAction(cls, x): - v = cls(*x) - return v - - def spec(self): - inner = self.val.encode("string_escape") - inner = inner.replace(r"\'", r"\x27") - return "'" + inner + "'" - - -class TokValueNakedLiteral(_TokValueLiteral): - - @classmethod - def expr(cls): - e = v_naked_literal.copy() - return e.setParseAction(lambda x: cls(*x)) - - def spec(self): - return self.val.encode("string_escape") - - -class TokValueGenerate(Token): - - def __init__(self, usize, unit, datatype): - if not unit: - unit = "b" - self.usize, self.unit, self.datatype = usize, unit, datatype - - def bytes(self): - return self.usize * utils.SIZE_UNITS[self.unit] - - def get_generator(self, settings_): - return generators.RandomGenerator(self.datatype, self.bytes()) - - def freeze(self, settings): - g = self.get_generator(settings) - return TokValueLiteral(g[:].encode("string_escape")) - - @classmethod - def expr(cls): - e = pp.Literal("@").suppress() + v_integer - - u = reduce( - operator.or_, - [pp.Literal(i) for i in utils.SIZE_UNITS.keys()] - ).leaveWhitespace() - e = e + pp.Optional(u, default=None) - - s = pp.Literal(",").suppress() - s += reduce( - operator.or_, - [pp.Literal(i) for i in generators.DATATYPES.keys()] - ) - e += pp.Optional(s, default="bytes") - return e.setParseAction(lambda x: cls(*x)) - - def spec(self): - s = "@%s" % self.usize - if self.unit != "b": - s += self.unit - if self.datatype != "bytes": - s += ",%s" % self.datatype - return s - - -class TokValueFile(Token): - - def __init__(self, path): - self.path = str(path) - - @classmethod - def expr(cls): - e = pp.Literal("<").suppress() - e = e + v_naked_literal - return e.setParseAction(lambda x: cls(*x)) - - def freeze(self, settings_): - return self - - def get_generator(self, settings): - if not settings.staticdir: - raise exceptions.FileAccessDenied("File access disabled.") - s = os.path.expanduser(self.path) - s = os.path.normpath( - os.path.abspath(os.path.join(settings.staticdir, s)) - ) - uf = settings.unconstrained_file_access - if not uf and not s.startswith(settings.staticdir): - raise exceptions.FileAccessDenied( - "File access outside of configured directory" - ) - if not os.path.isfile(s): - raise exceptions.FileAccessDenied("File not readable") - return generators.FileGenerator(s) - - def spec(self): - return "<'%s'" % self.path.encode("string_escape") - - -TokValue = pp.MatchFirst( - [ - TokValueGenerate.expr(), - TokValueFile.expr(), - TokValueLiteral.expr() - ] -) - - -TokNakedValue = pp.MatchFirst( - [ - TokValueGenerate.expr(), - TokValueFile.expr(), - TokValueLiteral.expr(), - TokValueNakedLiteral.expr(), - ] -) - - -TokOffset = pp.MatchFirst( - [ - v_integer, - pp.Literal("r"), - pp.Literal("a") - ] -) - - -class _Component(Token): - - """ - A value component of the primary specification of an message. - Components produce byte values desribe the bytes of the message. - """ - - def values(self, settings): # pragma: no cover - """ - A sequence of values, which can either be strings or generators. - """ - pass - - def string(self, settings=None): - """ - A string representation of the object. - """ - return "".join(i[:] for i in self.values(settings or {})) - - -class KeyValue(_Component): - - """ - A key/value pair. - cls.preamble: leader - """ - - def __init__(self, key, value): - self.key, self.value = key, value - - @classmethod - def expr(cls): - e = pp.Literal(cls.preamble).suppress() - e += TokValue - e += pp.Literal("=").suppress() - e += TokValue - return e.setParseAction(lambda x: cls(*x)) - - def spec(self): - return "%s%s=%s" % (self.preamble, self.key.spec(), self.value.spec()) - - def freeze(self, settings): - return self.__class__( - self.key.freeze(settings), self.value.freeze(settings) - ) - - -class CaselessLiteral(_Component): - - """ - A caseless token that can take only one value. - """ - - def __init__(self, value): - self.value = value - - @classmethod - def expr(cls): - spec = pp.CaselessLiteral(cls.TOK) - spec = spec.setParseAction(lambda x: cls(*x)) - return spec - - def values(self, settings): - return self.TOK - - def spec(self): - return self.TOK - - def freeze(self, settings_): - return self - - -class OptionsOrValue(_Component): - - """ - Can be any of a specified set of options, or a value specifier. - """ - preamble = "" - options = [] - - def __init__(self, value): - # If it's a string, we were passed one of the options, so we lower-case - # it to be canonical. The user can specify a different case by using a - # string value literal. - self.option_used = False - if isinstance(value, basestring): - for i in self.options: - # Find the exact option value in a case-insensitive way - if i.lower() == value.lower(): - self.option_used = True - value = TokValueLiteral(i) - break - self.value = value - - @classmethod - def expr(cls): - parts = [pp.CaselessLiteral(i) for i in cls.options] - m = pp.MatchFirst(parts) - spec = m | TokValue.copy() - spec = spec.setParseAction(lambda x: cls(*x)) - if cls.preamble: - spec = pp.Literal(cls.preamble).suppress() + spec - return spec - - def values(self, settings): - return [ - self.value.get_generator(settings) - ] - - def spec(self): - s = self.value.spec() - if s[1:-1].lower() in self.options: - s = s[1:-1].lower() - return "%s%s" % (self.preamble, s) - - def freeze(self, settings): - return self.__class__(self.value.freeze(settings)) - - -class Integer(_Component): - bounds = (None, None) - preamble = "" - - def __init__(self, value): - v = int(value) - outofbounds = any([ - self.bounds[0] is not None and v < self.bounds[0], - self.bounds[1] is not None and v > self.bounds[1] - ]) - if outofbounds: - raise exceptions.ParseException( - "Integer value must be between %s and %s." % self.bounds, - 0, 0 - ) - self.value = str(value) - - @classmethod - def expr(cls): - e = v_integer.copy() - if cls.preamble: - e = pp.Literal(cls.preamble).suppress() + e - return e.setParseAction(lambda x: cls(*x)) - - def values(self, settings): - return self.value - - def spec(self): - return "%s%s" % (self.preamble, self.value) - - def freeze(self, settings_): - return self - - -class Value(_Component): - - """ - A value component lead by an optional preamble. - """ - preamble = "" - - def __init__(self, value): - self.value = value - - @classmethod - def expr(cls): - e = (TokValue | TokNakedValue) - if cls.preamble: - e = pp.Literal(cls.preamble).suppress() + e - return e.setParseAction(lambda x: cls(*x)) - - def values(self, settings): - return [self.value.get_generator(settings)] - - def spec(self): - return "%s%s" % (self.preamble, self.value.spec()) - - def freeze(self, settings): - return self.__class__(self.value.freeze(settings)) - - -class FixedLengthValue(Value): - - """ - A value component lead by an optional preamble. - """ - preamble = "" - length = None - - def __init__(self, value): - Value.__init__(self, value) - lenguess = None - try: - lenguess = len(value.get_generator(Settings())) - except exceptions.RenderError: - pass - # This check will fail if we know the length upfront - if lenguess is not None and lenguess != self.length: - raise exceptions.RenderError( - "Invalid value length: '%s' is %s bytes, should be %s." % ( - self.spec(), - lenguess, - self.length - ) - ) - - def values(self, settings): - ret = Value.values(self, settings) - l = sum(len(i) for i in ret) - # This check will fail if we don't know the length upfront - i.e. for - # file inputs - if l != self.length: - raise exceptions.RenderError( - "Invalid value length: '%s' is %s bytes, should be %s." % ( - self.spec(), - l, - self.length - ) - ) - return ret - - -class Boolean(_Component): - - """ - A boolean flag. - name = true - -name = false - """ - name = "" - - def __init__(self, value): - self.value = value - - @classmethod - def expr(cls): - e = pp.Optional(pp.Literal("-"), default=True) - e += pp.Literal(cls.name).suppress() - - def parse(s_, loc_, toks): - val = True - if toks[0] == "-": - val = False - return cls(val) - - return e.setParseAction(parse) - - def spec(self): - return "%s%s" % ("-" if not self.value else "", self.name) - - -class IntField(_Component): - - """ - An integer field, where values can optionally specified by name. - """ - names = {} - max = 16 - preamble = "" - - def __init__(self, value): - self.origvalue = value - self.value = self.names.get(value, value) - if self.value > self.max: - raise exceptions.ParseException( - "Value can't exceed %s" % self.max, 0, 0 - ) - - @classmethod - def expr(cls): - parts = [pp.CaselessLiteral(i) for i in cls.names.keys()] - m = pp.MatchFirst(parts) - spec = m | v_integer.copy() - spec = spec.setParseAction(lambda x: cls(*x)) - if cls.preamble: - spec = pp.Literal(cls.preamble).suppress() + spec - return spec - - def values(self, settings): - return [str(self.value)] - - def spec(self): - return "%s%s" % (self.preamble, self.origvalue) - - -class NestedMessage(Token): - - """ - A nested message, as an escaped string with a preamble. - """ - preamble = "" - nest_type = None - - def __init__(self, value): - Token.__init__(self) - self.value = value - try: - self.parsed = self.nest_type( - self.nest_type.expr().parseString( - value.val, - parseAll=True - ) - ) - except pp.ParseException as v: - raise exceptions.ParseException(v.msg, v.line, v.col) - - @classmethod - def expr(cls): - e = pp.Literal(cls.preamble).suppress() - e = e + TokValueLiteral.expr() - return e.setParseAction(lambda x: cls(*x)) - - def values(self, settings): - return [ - self.value.get_generator(settings), - ] - - def spec(self): - return "%s%s" % (self.preamble, self.value.spec()) - - def freeze(self, settings): - f = self.parsed.freeze(settings).spec() - return self.__class__(TokValueLiteral(f.encode("string_escape"))) diff --git a/pathod/pathod/language/exceptions.py b/pathod/pathod/language/exceptions.py deleted file mode 100644 index 84ad3c02..00000000 --- a/pathod/pathod/language/exceptions.py +++ /dev/null @@ -1,22 +0,0 @@ - -class RenderError(Exception): - pass - - -class FileAccessDenied(RenderError): - pass - - -class ParseException(Exception): - - def __init__(self, msg, s, col): - Exception.__init__(self) - self.msg = msg - self.s = s - self.col = col - - def marked(self): - return "%s\n%s" % (self.s, " " * (self.col - 1) + "^") - - def __str__(self): - return "%s at char %s" % (self.msg, self.col) diff --git a/pathod/pathod/language/generators.py b/pathod/pathod/language/generators.py deleted file mode 100644 index a17e7052..00000000 --- a/pathod/pathod/language/generators.py +++ /dev/null @@ -1,86 +0,0 @@ -import string -import random -import mmap - -DATATYPES = dict( - ascii_letters=string.ascii_letters, - ascii_lowercase=string.ascii_lowercase, - ascii_uppercase=string.ascii_uppercase, - digits=string.digits, - hexdigits=string.hexdigits, - octdigits=string.octdigits, - punctuation=string.punctuation, - whitespace=string.whitespace, - ascii=string.printable, - bytes="".join(chr(i) for i in range(256)) -) - - -class TransformGenerator(object): - - """ - Perform a byte-by-byte transform another generator - that is, for each - input byte, the transformation must produce one output byte. - - gen: A generator to wrap - transform: A function (offset, data) -> transformed - """ - - def __init__(self, gen, transform): - self.gen = gen - self.transform = transform - - def __len__(self): - return len(self.gen) - - def __getitem__(self, x): - d = self.gen.__getitem__(x) - return self.transform(x, d) - - def __getslice__(self, a, b): - d = self.gen.__getslice__(a, b) - return self.transform(a, d) - - def __repr__(self): - return "'transform(%s)'" % self.gen - - -class RandomGenerator(object): - - def __init__(self, dtype, length): - self.dtype = dtype - self.length = length - - def __len__(self): - return self.length - - def __getitem__(self, x): - return random.choice(DATATYPES[self.dtype]) - - def __getslice__(self, a, b): - b = min(b, self.length) - chars = DATATYPES[self.dtype] - return "".join(random.choice(chars) for x in range(a, b)) - - def __repr__(self): - return "%s random from %s" % (self.length, self.dtype) - - -class FileGenerator(object): - - def __init__(self, path): - self.path = path - self.fp = file(path, "rb") - self.map = mmap.mmap(self.fp.fileno(), 0, access=mmap.ACCESS_READ) - - def __len__(self): - return len(self.map) - - def __getitem__(self, x): - return self.map.__getitem__(x) - - def __getslice__(self, a, b): - return self.map.__getslice__(a, b) - - def __repr__(self): - return "<%s" % self.path diff --git a/pathod/pathod/language/http.py b/pathod/pathod/language/http.py deleted file mode 100644 index a82f12fe..00000000 --- a/pathod/pathod/language/http.py +++ /dev/null @@ -1,381 +0,0 @@ - -import abc - -import pyparsing as pp - -import netlib.websockets -from netlib.http import status_codes, user_agents -from . import base, exceptions, actions, message - -# TODO: use netlib.semantics.protocol assemble method, -# instead of duplicating the HTTP on-the-wire representation here. -# see http2 language for an example - -class WS(base.CaselessLiteral): - TOK = "ws" - - -class Raw(base.CaselessLiteral): - TOK = "r" - - -class Path(base.Value): - pass - - -class StatusCode(base.Integer): - pass - - -class Reason(base.Value): - preamble = "m" - - -class Body(base.Value): - preamble = "b" - - -class Times(base.Integer): - preamble = "x" - - -class Method(base.OptionsOrValue): - options = [ - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "OPTIONS", - "TRACE", - "CONNECT", - ] - - -class _HeaderMixin(object): - unique_name = None - - def format_header(self, key, value): - return [key, ": ", value, "\r\n"] - - def values(self, settings): - return self.format_header( - self.key.get_generator(settings), - self.value.get_generator(settings), - ) - - -class Header(_HeaderMixin, base.KeyValue): - preamble = "h" - - -class ShortcutContentType(_HeaderMixin, base.Value): - preamble = "c" - key = base.TokValueLiteral("Content-Type") - - -class ShortcutLocation(_HeaderMixin, base.Value): - preamble = "l" - key = base.TokValueLiteral("Location") - - -class ShortcutUserAgent(_HeaderMixin, base.OptionsOrValue): - preamble = "u" - options = [i[1] for i in user_agents.UASTRINGS] - key = base.TokValueLiteral("User-Agent") - - def values(self, settings): - value = self.value.val - if self.option_used: - value = user_agents.get_by_shortcut(value.lower())[2] - - return self.format_header( - self.key.get_generator(settings), - value - ) - - -def get_header(val, headers): - """ - Header keys may be Values, so we have to "generate" them as we try the - match. - """ - for h in headers: - k = h.key.get_generator({}) - if len(k) == len(val) and k[:].lower() == val.lower(): - return h - return None - - -class _HTTPMessage(message.Message): - version = "HTTP/1.1" - - @property - def actions(self): - return self.toks(actions._Action) - - @property - def raw(self): - return bool(self.tok(Raw)) - - @property - def body(self): - return self.tok(Body) - - @abc.abstractmethod - def preamble(self, settings): # pragma: no cover - pass - - @property - def headers(self): - return self.toks(_HeaderMixin) - - def values(self, settings): - vals = self.preamble(settings) - vals.append("\r\n") - for h in self.headers: - vals.extend(h.values(settings)) - vals.append("\r\n") - if self.body: - vals.extend(self.body.values(settings)) - return vals - - -class Response(_HTTPMessage): - unique_name = None - comps = ( - Header, - ShortcutContentType, - ShortcutLocation, - Raw, - Reason, - Body, - - actions.PauseAt, - actions.DisconnectAt, - actions.InjectAt, - ) - logattrs = ["status_code", "reason", "version", "body"] - - @property - def ws(self): - return self.tok(WS) - - @property - def status_code(self): - return self.tok(StatusCode) - - @property - def reason(self): - return self.tok(Reason) - - def preamble(self, settings): - l = [self.version, " "] - l.extend(self.status_code.values(settings)) - status_code = int(self.status_code.value) - l.append(" ") - if self.reason: - l.extend(self.reason.values(settings)) - else: - l.append( - status_codes.RESPONSES.get( - status_code, - "Unknown code" - ) - ) - return l - - def resolve(self, settings, msg=None): - tokens = self.tokens[:] - if self.ws: - if not settings.websocket_key: - raise exceptions.RenderError( - "No websocket key - have we seen a client handshake?" - ) - if not self.status_code: - tokens.insert( - 1, - StatusCode(101) - ) - headers = netlib.websockets.WebsocketsProtocol.server_handshake_headers( - settings.websocket_key - ) - for i in headers.fields: - if not get_header(i[0], self.headers): - tokens.append( - Header( - base.TokValueLiteral(i[0]), - base.TokValueLiteral(i[1])) - ) - if not self.raw: - if not get_header("Content-Length", self.headers): - if not self.body: - length = 0 - else: - length = sum( - len(i) for i in self.body.values(settings) - ) - tokens.append( - Header( - base.TokValueLiteral("Content-Length"), - base.TokValueLiteral(str(length)), - ) - ) - intermediate = self.__class__(tokens) - return self.__class__( - [i.resolve(settings, intermediate) for i in tokens] - ) - - @classmethod - def expr(cls): - parts = [i.expr() for i in cls.comps] - atom = pp.MatchFirst(parts) - resp = pp.And( - [ - pp.MatchFirst( - [ - WS.expr() + pp.Optional( - base.Sep + StatusCode.expr() - ), - StatusCode.expr(), - ] - ), - pp.ZeroOrMore(base.Sep + atom) - ] - ) - resp = resp.setParseAction(cls) - return resp - - def spec(self): - return ":".join([i.spec() for i in self.tokens]) - - -class NestedResponse(base.NestedMessage): - preamble = "s" - nest_type = Response - - -class Request(_HTTPMessage): - comps = ( - Header, - ShortcutContentType, - ShortcutUserAgent, - Raw, - NestedResponse, - Body, - Times, - - actions.PauseAt, - actions.DisconnectAt, - actions.InjectAt, - ) - logattrs = ["method", "path", "body"] - - @property - def ws(self): - return self.tok(WS) - - @property - def method(self): - return self.tok(Method) - - @property - def path(self): - return self.tok(Path) - - @property - def times(self): - return self.tok(Times) - - @property - def nested_response(self): - return self.tok(NestedResponse) - - def preamble(self, settings): - v = self.method.values(settings) - v.append(" ") - v.extend(self.path.values(settings)) - if self.nested_response: - v.append(self.nested_response.parsed.spec()) - v.append(" ") - v.append(self.version) - return v - - def resolve(self, settings, msg=None): - tokens = self.tokens[:] - if self.ws: - if not self.method: - tokens.insert( - 1, - Method("get") - ) - for i in netlib.websockets.WebsocketsProtocol.client_handshake_headers().fields: - if not get_header(i[0], self.headers): - tokens.append( - Header( - base.TokValueLiteral(i[0]), - base.TokValueLiteral(i[1]) - ) - ) - if not self.raw: - if not get_header("Content-Length", self.headers): - if self.body: - length = sum( - len(i) for i in self.body.values(settings) - ) - tokens.append( - Header( - base.TokValueLiteral("Content-Length"), - base.TokValueLiteral(str(length)), - ) - ) - if settings.request_host: - if not get_header("Host", self.headers): - tokens.append( - Header( - base.TokValueLiteral("Host"), - base.TokValueLiteral(settings.request_host) - ) - ) - intermediate = self.__class__(tokens) - return self.__class__( - [i.resolve(settings, intermediate) for i in tokens] - ) - - @classmethod - def expr(cls): - parts = [i.expr() for i in cls.comps] - atom = pp.MatchFirst(parts) - resp = pp.And( - [ - pp.MatchFirst( - [ - WS.expr() + pp.Optional( - base.Sep + Method.expr() - ), - Method.expr(), - ] - ), - base.Sep, - Path.expr(), - pp.ZeroOrMore(base.Sep + atom) - ] - ) - resp = resp.setParseAction(cls) - return resp - - def spec(self): - return ":".join([i.spec() for i in self.tokens]) - - -def make_error_response(reason, body=None): - tokens = [ - StatusCode("800"), - Header( - base.TokValueLiteral("Content-Type"), - base.TokValueLiteral("text/plain") - ), - Reason(base.TokValueLiteral(reason)), - Body(base.TokValueLiteral("pathod error: " + (body or reason))), - ] - return Response(tokens) diff --git a/pathod/pathod/language/http2.py b/pathod/pathod/language/http2.py deleted file mode 100644 index d5e3ca31..00000000 --- a/pathod/pathod/language/http2.py +++ /dev/null @@ -1,299 +0,0 @@ -import pyparsing as pp - -from netlib import http -from netlib.http import user_agents, Headers -from . import base, message - -""" - Normal HTTP requests: - ::
    : - e.g.: - GET:/ - GET:/:h"foo"="bar" - POST:/:h"foo"="bar":b'content body payload' - - Normal HTTP responses: - :
    : - e.g.: - 200 - 302:h"foo"="bar" - 404:h"foo"="bar":b'content body payload' - - Individual HTTP/2 frames: - h2f::::: - e.g.: - h2f:0:PING - h2f:42:HEADERS:END_HEADERS:0x1234567:foo=bar,host=example.com - h2f:42:DATA:END_STREAM,PADDED:0x1234567:'content body payload' -""" - -def get_header(val, headers): - """ - Header keys may be Values, so we have to "generate" them as we try the - match. - """ - for h in headers: - k = h.key.get_generator({}) - if len(k) == len(val) and k[:].lower() == val.lower(): - return h - return None - - -class _HeaderMixin(object): - unique_name = None - - def values(self, settings): - return ( - self.key.get_generator(settings), - self.value.get_generator(settings), - ) - -class _HTTP2Message(message.Message): - @property - def actions(self): - return [] # self.toks(actions._Action) - - @property - def headers(self): - headers = self.toks(_HeaderMixin) - - if not self.raw: - if not get_header("content-length", headers): - if not self.body: - length = 0 - else: - length = len(self.body.string()) - headers.append( - Header( - base.TokValueLiteral("content-length"), - base.TokValueLiteral(str(length)), - ) - ) - return headers - - @property - def raw(self): - return bool(self.tok(Raw)) - - @property - def body(self): - return self.tok(Body) - - def resolve(self, settings): - return self - - -class StatusCode(base.Integer): - pass - - -class Method(base.OptionsOrValue): - options = [ - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - ] - - -class Path(base.Value): - pass - - -class Header(_HeaderMixin, base.KeyValue): - preamble = "h" - - -class ShortcutContentType(_HeaderMixin, base.Value): - preamble = "c" - key = base.TokValueLiteral("content-type") - - -class ShortcutLocation(_HeaderMixin, base.Value): - preamble = "l" - key = base.TokValueLiteral("location") - - -class ShortcutUserAgent(_HeaderMixin, base.OptionsOrValue): - preamble = "u" - options = [i[1] for i in user_agents.UASTRINGS] - key = base.TokValueLiteral("user-agent") - - def values(self, settings): - value = self.value.val - if self.option_used: - value = user_agents.get_by_shortcut(value.lower())[2] - - return ( - self.key.get_generator(settings), - value - ) - - -class Raw(base.CaselessLiteral): - TOK = "r" - - -class Body(base.Value): - preamble = "b" - - -class Times(base.Integer): - preamble = "x" - - -class Response(_HTTP2Message): - unique_name = None - comps = ( - Header, - Body, - ShortcutContentType, - ShortcutLocation, - Raw, - ) - - def __init__(self, tokens): - super(Response, self).__init__(tokens) - self.rendered_values = None - self.stream_id = 2 - - @property - def status_code(self): - return self.tok(StatusCode) - - @classmethod - def expr(cls): - parts = [i.expr() for i in cls.comps] - atom = pp.MatchFirst(parts) - resp = pp.And( - [ - StatusCode.expr(), - pp.ZeroOrMore(base.Sep + atom) - ] - ) - resp = resp.setParseAction(cls) - return resp - - def values(self, settings): - if self.rendered_values: - return self.rendered_values - else: - headers = Headers([header.values(settings) for header in self.headers]) - - body = self.body - if body: - body = body.string() - - resp = http.Response( - (2, 0), - self.status_code.string(), - '', - headers, - body, - ) - resp.stream_id = self.stream_id - - self.rendered_values = settings.protocol.assemble(resp) - return self.rendered_values - - def spec(self): - return ":".join([i.spec() for i in self.tokens]) - - -class NestedResponse(base.NestedMessage): - preamble = "s" - nest_type = Response - - -class Request(_HTTP2Message): - comps = ( - Header, - ShortcutContentType, - ShortcutUserAgent, - Raw, - NestedResponse, - Body, - Times, - ) - logattrs = ["method", "path"] - - def __init__(self, tokens): - super(Request, self).__init__(tokens) - self.rendered_values = None - self.stream_id = 1 - - @property - def method(self): - return self.tok(Method) - - @property - def path(self): - return self.tok(Path) - - @property - def nested_response(self): - return self.tok(NestedResponse) - - @property - def times(self): - return self.tok(Times) - - @classmethod - def expr(cls): - parts = [i.expr() for i in cls.comps] - atom = pp.MatchFirst(parts) - resp = pp.And( - [ - Method.expr(), - base.Sep, - Path.expr(), - pp.ZeroOrMore(base.Sep + atom) - ] - ) - resp = resp.setParseAction(cls) - return resp - - def values(self, settings): - if self.rendered_values: - return self.rendered_values - else: - path = self.path.string() - if self.nested_response: - path += self.nested_response.parsed.spec() - - headers = Headers([header.values(settings) for header in self.headers]) - - body = self.body - if body: - body = body.string() - - req = http.Request( - '', - self.method.string(), - '', - '', - '', - path, - (2, 0), - headers, - body, - ) - req.stream_id = self.stream_id - - self.rendered_values = settings.protocol.assemble(req) - return self.rendered_values - - def spec(self): - return ":".join([i.spec() for i in self.tokens]) - -def make_error_response(reason, body=None): - tokens = [ - StatusCode("800"), - Body(base.TokValueLiteral("pathod error: " + (body or reason))), - ] - return Response(tokens) - - -# class Frame(message.Message): -# pass diff --git a/pathod/pathod/language/message.py b/pathod/pathod/language/message.py deleted file mode 100644 index 33124856..00000000 --- a/pathod/pathod/language/message.py +++ /dev/null @@ -1,96 +0,0 @@ -import abc -from . import actions, exceptions - -LOG_TRUNCATE = 1024 - - -class Message(object): - __metaclass__ = abc.ABCMeta - logattrs = [] - - def __init__(self, tokens): - track = set([]) - for i in tokens: - if i.unique_name: - if i.unique_name in track: - raise exceptions.ParseException( - "Message has multiple %s clauses, " - "but should only have one." % i.unique_name, - 0, 0 - ) - else: - track.add(i.unique_name) - self.tokens = tokens - - def strike_token(self, name): - toks = [i for i in self.tokens if i.unique_name != name] - return self.__class__(toks) - - def toks(self, klass): - """ - Fetch all tokens that are instances of klass - """ - return [i for i in self.tokens if isinstance(i, klass)] - - def tok(self, klass): - """ - Fetch first token that is an instance of klass - """ - l = self.toks(klass) - if l: - return l[0] - - def length(self, settings): - """ - Calculate the length of the base message without any applied - actions. - """ - return sum(len(x) for x in self.values(settings)) - - def preview_safe(self): - """ - Return a copy of this message that issafe for previews. - """ - tokens = [i for i in self.tokens if not isinstance(i, actions.PauseAt)] - return self.__class__(tokens) - - def maximum_length(self, settings): - """ - Calculate the maximum length of the base message with all applied - actions. - """ - l = self.length(settings) - for i in self.actions: - if isinstance(i, actions.InjectAt): - l += len(i.value.get_generator(settings)) - return l - - @classmethod - def expr(cls): # pragma: no cover - pass - - def log(self, settings): - """ - A dictionary that should be logged if this message is served. - """ - ret = {} - for i in self.logattrs: - v = getattr(self, i) - # Careful not to log any VALUE specs without sanitizing them first. - # We truncate at 1k. - if hasattr(v, "values"): - v = [x[:LOG_TRUNCATE] for x in v.values(settings)] - v = "".join(v).encode("string_escape") - elif hasattr(v, "__len__"): - v = v[:LOG_TRUNCATE] - v = v.encode("string_escape") - ret[i] = v - ret["spec"] = self.spec() - return ret - - def freeze(self, settings): - r = self.resolve(settings) - return self.__class__([i.freeze(settings) for i in r.tokens]) - - def __repr__(self): - return self.spec() diff --git a/pathod/pathod/language/websockets.py b/pathod/pathod/language/websockets.py deleted file mode 100644 index ea7c870e..00000000 --- a/pathod/pathod/language/websockets.py +++ /dev/null @@ -1,241 +0,0 @@ -import os -import netlib.websockets -import pyparsing as pp -from . import base, generators, actions, message - -NESTED_LEADER = "pathod!" - - -class WF(base.CaselessLiteral): - TOK = "wf" - - -class OpCode(base.IntField): - names = { - "continue": netlib.websockets.OPCODE.CONTINUE, - "text": netlib.websockets.OPCODE.TEXT, - "binary": netlib.websockets.OPCODE.BINARY, - "close": netlib.websockets.OPCODE.CLOSE, - "ping": netlib.websockets.OPCODE.PING, - "pong": netlib.websockets.OPCODE.PONG, - } - max = 15 - preamble = "c" - - -class Body(base.Value): - preamble = "b" - - -class RawBody(base.Value): - unique_name = "body" - preamble = "r" - - -class Fin(base.Boolean): - name = "fin" - - -class RSV1(base.Boolean): - name = "rsv1" - - -class RSV2(base.Boolean): - name = "rsv2" - - -class RSV3(base.Boolean): - name = "rsv3" - - -class Mask(base.Boolean): - name = "mask" - - -class Key(base.FixedLengthValue): - preamble = "k" - length = 4 - - -class KeyNone(base.CaselessLiteral): - unique_name = "key" - TOK = "knone" - - -class Length(base.Integer): - bounds = (0, 1 << 64) - preamble = "l" - - -class Times(base.Integer): - preamble = "x" - - -COMPONENTS = ( - OpCode, - Length, - # Bit flags - Fin, - RSV1, - RSV2, - RSV3, - Mask, - actions.PauseAt, - actions.DisconnectAt, - actions.InjectAt, - KeyNone, - Key, - Times, - - Body, - RawBody, -) - - -class WebsocketFrame(message.Message): - components = COMPONENTS - logattrs = ["body"] - # Used for nested frames - unique_name = "body" - - @property - def actions(self): - return self.toks(actions._Action) - - @property - def body(self): - return self.tok(Body) - - @property - def rawbody(self): - return self.tok(RawBody) - - @property - def opcode(self): - return self.tok(OpCode) - - @property - def fin(self): - return self.tok(Fin) - - @property - def rsv1(self): - return self.tok(RSV1) - - @property - def rsv2(self): - return self.tok(RSV2) - - @property - def rsv3(self): - return self.tok(RSV3) - - @property - def mask(self): - return self.tok(Mask) - - @property - def key(self): - return self.tok(Key) - - @property - def knone(self): - return self.tok(KeyNone) - - @property - def times(self): - return self.tok(Times) - - @property - def toklength(self): - return self.tok(Length) - - @classmethod - def expr(cls): - parts = [i.expr() for i in cls.components] - atom = pp.MatchFirst(parts) - resp = pp.And( - [ - WF.expr(), - base.Sep, - pp.ZeroOrMore(base.Sep + atom) - ] - ) - resp = resp.setParseAction(cls) - return resp - - @property - def nested_frame(self): - return self.tok(NestedFrame) - - def resolve(self, settings, msg=None): - tokens = self.tokens[:] - if not self.mask and settings.is_client: - tokens.append( - Mask(True) - ) - if not self.knone and self.mask and self.mask.value and not self.key: - tokens.append( - Key(base.TokValueLiteral(os.urandom(4))) - ) - return self.__class__( - [i.resolve(settings, self) for i in tokens] - ) - - def values(self, settings): - if self.body: - bodygen = self.body.value.get_generator(settings) - length = len(self.body.value.get_generator(settings)) - elif self.rawbody: - bodygen = self.rawbody.value.get_generator(settings) - length = len(self.rawbody.value.get_generator(settings)) - elif self.nested_frame: - bodygen = NESTED_LEADER + self.nested_frame.parsed.spec() - length = len(bodygen) - else: - bodygen = None - length = 0 - if self.toklength: - length = int(self.toklength.value) - frameparts = dict( - payload_length=length - ) - if self.mask and self.mask.value: - frameparts["mask"] = True - if self.knone: - frameparts["masking_key"] = None - elif self.key: - key = self.key.values(settings)[0][:] - frameparts["masking_key"] = key - for i in ["opcode", "fin", "rsv1", "rsv2", "rsv3", "mask"]: - v = getattr(self, i, None) - if v is not None: - frameparts[i] = v.value - frame = netlib.websockets.FrameHeader(**frameparts) - vals = [bytes(frame)] - if bodygen: - if frame.masking_key and not self.rawbody: - masker = netlib.websockets.Masker(frame.masking_key) - vals.append( - generators.TransformGenerator( - bodygen, - masker.mask - ) - ) - else: - vals.append(bodygen) - return vals - - def spec(self): - return ":".join([i.spec() for i in self.tokens]) - - -class NestedFrame(base.NestedMessage): - preamble = "f" - nest_type = WebsocketFrame - - -class WebsocketClientFrame(WebsocketFrame): - components = COMPONENTS + ( - NestedFrame, - ) diff --git a/pathod/pathod/language/writer.py b/pathod/pathod/language/writer.py deleted file mode 100644 index 1a27e1ef..00000000 --- a/pathod/pathod/language/writer.py +++ /dev/null @@ -1,67 +0,0 @@ -import time -from netlib.exceptions import TcpDisconnect -import netlib.tcp - -BLOCKSIZE = 1024 -# It's not clear what the upper limit for time.sleep is. It's lower than the -# maximum int or float. 1 year should do. -FOREVER = 60 * 60 * 24 * 365 - - -def send_chunk(fp, val, blocksize, start, end): - """ - (start, end): Inclusive lower bound, exclusive upper bound. - """ - for i in range(start, end, blocksize): - fp.write( - val[i:min(i + blocksize, end)] - ) - return end - start - - -def write_values(fp, vals, actions, sofar=0, blocksize=BLOCKSIZE): - """ - vals: A list of values, which may be strings or Value objects. - - actions: A list of (offset, action, arg) tuples. Action may be "pause" - or "disconnect". - - Both vals and actions are in reverse order, with the first items last. - - Return True if connection should disconnect. - """ - sofar = 0 - try: - while vals: - v = vals.pop() - offset = 0 - while actions and actions[-1][0] < (sofar + len(v)): - a = actions.pop() - offset += send_chunk( - fp, - v, - blocksize, - offset, - a[0] - sofar - offset - ) - if a[1] == "pause": - time.sleep( - FOREVER if a[2] == "f" else a[2] - ) - elif a[1] == "disconnect": - return True - elif a[1] == "inject": - send_chunk(fp, a[2], blocksize, 0, len(a[2])) - send_chunk(fp, v, blocksize, offset, len(v)) - sofar += len(v) - # Remainders - while actions: - a = actions.pop() - if a[1] == "pause": - time.sleep(a[2]) - elif a[1] == "disconnect": - return True - elif a[1] == "inject": - send_chunk(fp, a[2], blocksize, 0, len(a[2])) - except TcpDisconnect: # pragma: no cover - return True diff --git a/pathod/pathod/log.py b/pathod/pathod/log.py deleted file mode 100644 index f203542f..00000000 --- a/pathod/pathod/log.py +++ /dev/null @@ -1,83 +0,0 @@ -import datetime - -import netlib.utils -import netlib.tcp -import netlib.http - -TIMEFMT = '%d-%m-%y %H:%M:%S' - - -def write_raw(fp, lines): - if fp: - fp.write( - "%s: " % datetime.datetime.now().strftime(TIMEFMT) - ) - for i in lines: - fp.write(i) - fp.write("\n") - fp.flush() - - -class LogCtx(object): - - def __init__(self, fp, hex, rfile, wfile): - self.lines = [] - self.fp = fp - self.suppressed = False - self.hex = hex - self.rfile, self.wfile = rfile, wfile - - def __enter__(self): - if self.wfile: - self.wfile.start_log() - if self.rfile: - self.rfile.start_log() - return self - - def __exit__(self, exc_type, exc_value, traceback): - wlog = self.wfile.get_log() if self.wfile else None - rlog = self.rfile.get_log() if self.rfile else None - if self.suppressed or not self.fp: - return - if wlog: - self("Bytes written:") - self.dump(wlog, self.hex) - if rlog: - self("Bytes read:") - self.dump(rlog, self.hex) - if self.lines: - write_raw( - self.fp, - [ - "\n".join(self.lines), - ] - ) - if exc_value: - raise exc_type, exc_value, traceback - - def suppress(self): - self.suppressed = True - - def dump(self, data, hexdump): - if hexdump: - for line in netlib.utils.hexdump(data): - self("\t%s %s %s" % line) - else: - for i in netlib.utils.clean_bin(data).split("\n"): - self("\t%s" % i) - - def __call__(self, line): - self.lines.append(line) - - -class ConnectionLogger: - def __init__(self, fp, hex, rfile, wfile): - self.fp = fp - self.hex = hex - self.rfile, self.wfile = rfile, wfile - - def ctx(self): - return LogCtx(self.fp, self.hex, self.rfile, self.wfile) - - def write(self, lines): - write_raw(self.fp, lines) diff --git a/pathod/pathod/pathoc.py b/pathod/pathod/pathoc.py deleted file mode 100644 index c0a33b62..00000000 --- a/pathod/pathod/pathoc.py +++ /dev/null @@ -1,534 +0,0 @@ -import contextlib -import sys -import os -import itertools -import hashlib -import Queue -import random -import select -import time -import threading - -import OpenSSL.crypto -import six - -from netlib import tcp, http, certutils, websockets, socks -from netlib.exceptions import HttpException, TcpDisconnect, TcpTimeout, TlsException, TcpException, \ - NetlibException -from netlib.http import http1, http2 - -import language.http -import language.websockets -from . import utils, log - -import logging -from netlib.tutils import treq - -logging.getLogger("hpack").setLevel(logging.WARNING) - - -class PathocError(Exception): - pass - - -class SSLInfo(object): - - def __init__(self, certchain, cipher, alp): - self.certchain, self.cipher, self.alp = certchain, cipher, alp - - def __str__(self): - parts = [ - "Application Layer Protocol: %s" % self.alp, - "Cipher: %s, %s bit, %s" % self.cipher, - "SSL certificate chain:" - ] - for i in self.certchain: - parts.append("\tSubject: ") - for cn in i.get_subject().get_components(): - parts.append("\t\t%s=%s" % cn) - parts.append("\tIssuer: ") - for cn in i.get_issuer().get_components(): - parts.append("\t\t%s=%s" % cn) - parts.extend( - [ - "\tVersion: %s" % i.get_version(), - "\tValidity: %s - %s" % ( - i.get_notBefore(), i.get_notAfter() - ), - "\tSerial: %s" % i.get_serial_number(), - "\tAlgorithm: %s" % i.get_signature_algorithm() - ] - ) - pk = i.get_pubkey() - types = { - OpenSSL.crypto.TYPE_RSA: "RSA", - OpenSSL.crypto.TYPE_DSA: "DSA" - } - t = types.get(pk.type(), "Uknown") - parts.append("\tPubkey: %s bit %s" % (pk.bits(), t)) - s = certutils.SSLCert(i) - if s.altnames: - parts.append("\tSANs: %s" % " ".join(s.altnames)) - return "\n".join(parts) - - - -class WebsocketFrameReader(threading.Thread): - - def __init__( - self, - rfile, - logfp, - showresp, - hexdump, - ws_read_limit, - timeout - ): - threading.Thread.__init__(self) - self.timeout = timeout - self.ws_read_limit = ws_read_limit - self.logfp = logfp - self.showresp = showresp - self.hexdump = hexdump - self.rfile = rfile - self.terminate = Queue.Queue() - self.frames_queue = Queue.Queue() - self.logger = log.ConnectionLogger( - self.logfp, - self.hexdump, - rfile if showresp else None, - None - ) - - @contextlib.contextmanager - def terminator(self): - yield - self.frames_queue.put(None) - - def run(self): - starttime = time.time() - with self.terminator(): - while True: - if self.ws_read_limit == 0: - return - r, _, _ = select.select([self.rfile], [], [], 0.05) - delta = time.time() - starttime - if not r and self.timeout and delta > self.timeout: - return - try: - self.terminate.get_nowait() - return - except Queue.Empty: - pass - for rfile in r: - with self.logger.ctx() as log: - try: - frm = websockets.Frame.from_file(self.rfile) - except TcpDisconnect: - return - self.frames_queue.put(frm) - log("<< %s" % frm.header.human_readable()) - if self.ws_read_limit is not None: - self.ws_read_limit -= 1 - starttime = time.time() - - -class Pathoc(tcp.TCPClient): - - def __init__( - self, - address, - - # SSL - ssl=None, - sni=None, - ssl_version=tcp.SSL_DEFAULT_METHOD, - ssl_options=tcp.SSL_DEFAULT_OPTIONS, - clientcert=None, - ciphers=None, - - # HTTP/2 - use_http2=False, - http2_skip_connection_preface=False, - http2_framedump=False, - - # Websockets - ws_read_limit=None, - - # Network - timeout=None, - - # Output control - showreq=False, - showresp=False, - explain=False, - hexdump=False, - ignorecodes=(), - ignoretimeout=False, - showsummary=False, - fp=sys.stdout - ): - """ - spec: A request specification - showreq: Print requests - showresp: Print responses - explain: Print request explanation - showssl: Print info on SSL connection - hexdump: When printing requests or responses, use hex dump output - showsummary: Show a summary of requests - ignorecodes: Sequence of return codes to ignore - """ - tcp.TCPClient.__init__(self, address) - - self.ssl, self.sni = ssl, sni - self.clientcert = clientcert - self.ssl_version = ssl_version - self.ssl_options = ssl_options - self.ciphers = ciphers - self.sslinfo = None - - self.use_http2 = use_http2 - self.http2_skip_connection_preface = http2_skip_connection_preface - self.http2_framedump = http2_framedump - - self.ws_read_limit = ws_read_limit - - self.timeout = timeout - - self.showreq = showreq - self.showresp = showresp - self.explain = explain - self.hexdump = hexdump - self.ignorecodes = ignorecodes - self.ignoretimeout = ignoretimeout - self.showsummary = showsummary - self.fp = fp - - self.ws_framereader = None - - if self.use_http2: - if not tcp.HAS_ALPN: # pragma: nocover - log.write_raw( - self.fp, - "HTTP/2 requires ALPN support. " - "Please use OpenSSL >= 1.0.2. " - "Pathoc might not be working as expected without ALPN." - ) - self.protocol = http2.HTTP2Protocol(self, dump_frames=self.http2_framedump) - else: - self.protocol = http1 - - self.settings = language.Settings( - is_client=True, - staticdir=os.getcwd(), - unconstrained_file_access=True, - request_host=self.address.host, - protocol=self.protocol, - ) - - def http_connect(self, connect_to): - self.wfile.write( - 'CONNECT %s:%s HTTP/1.1\r\n' % tuple(connect_to) + - '\r\n' - ) - self.wfile.flush() - try: - resp = self.protocol.read_response(self.rfile, treq(method="CONNECT")) - if resp.status_code != 200: - raise HttpException("Unexpected status code: %s" % resp.status_code) - except HttpException as e: - six.reraise(PathocError, PathocError( - "Proxy CONNECT failed: %s" % repr(e) - )) - - def socks_connect(self, connect_to): - try: - client_greet = socks.ClientGreeting(socks.VERSION.SOCKS5, [socks.METHOD.NO_AUTHENTICATION_REQUIRED]) - client_greet.to_file(self.wfile) - self.wfile.flush() - - server_greet = socks.ServerGreeting.from_file(self.rfile) - server_greet.assert_socks5() - if server_greet.method != socks.METHOD.NO_AUTHENTICATION_REQUIRED: - raise socks.SocksError( - socks.METHOD.NO_ACCEPTABLE_METHODS, - "pathoc only supports SOCKS without authentication" - ) - - connect_request = socks.Message( - socks.VERSION.SOCKS5, - socks.CMD.CONNECT, - socks.ATYP.DOMAINNAME, - tcp.Address.wrap(connect_to) - ) - connect_request.to_file(self.wfile) - self.wfile.flush() - - connect_reply = socks.Message.from_file(self.rfile) - connect_reply.assert_socks5() - if connect_reply.msg != socks.REP.SUCCEEDED: - raise socks.SocksError( - connect_reply.msg, - "SOCKS server error" - ) - except (socks.SocksError, TcpDisconnect) as e: - raise PathocError(str(e)) - - def connect(self, connect_to=None, showssl=False, fp=sys.stdout): - """ - connect_to: A (host, port) tuple, which will be connected to with - an HTTP CONNECT request. - """ - if self.use_http2 and not self.ssl: - raise NotImplementedError("HTTP2 without SSL is not supported.") - - tcp.TCPClient.connect(self) - - if connect_to: - self.http_connect(connect_to) - - self.sslinfo = None - if self.ssl: - try: - alpn_protos = [b'http/1.1'] - if self.use_http2: - alpn_protos.append(b'h2') - - self.convert_to_ssl( - sni=self.sni, - cert=self.clientcert, - method=self.ssl_version, - options=self.ssl_options, - cipher_list=self.ciphers, - alpn_protos=alpn_protos - ) - except TlsException as v: - raise PathocError(str(v)) - - self.sslinfo = SSLInfo( - self.connection.get_peer_cert_chain(), - self.get_current_cipher(), - self.get_alpn_proto_negotiated() - ) - if showssl: - print >> fp, str(self.sslinfo) - - if self.use_http2: - self.protocol.check_alpn() - if not self.http2_skip_connection_preface: - self.protocol.perform_client_connection_preface() - - if self.timeout: - self.settimeout(self.timeout) - - def stop(self): - if self.ws_framereader: - self.ws_framereader.terminate.put(None) - - def wait(self, timeout=0.01, finish=True): - """ - A generator that yields frames until Pathoc terminates. - - timeout: If specified None may be yielded instead if timeout is - reached. If timeout is None, wait forever. If timeout is 0, return - immedately if nothing is on the queue. - - finish: If true, consume messages until the reader shuts down. - Otherwise, return None on timeout. - """ - if self.ws_framereader: - while True: - try: - frm = self.ws_framereader.frames_queue.get( - timeout=timeout, - block=True if timeout != 0 else False - ) - except Queue.Empty: - if finish: - continue - else: - return - if frm is None: - self.ws_framereader.join() - return - yield frm - - def websocket_send_frame(self, r): - """ - Sends a single websocket frame. - """ - logger = log.ConnectionLogger( - self.fp, - self.hexdump, - None, - self.wfile if self.showreq else None, - ) - with logger.ctx() as lg: - lg(">> %s" % r) - language.serve(r, self.wfile, self.settings) - self.wfile.flush() - - def websocket_start(self, r): - """ - Performs an HTTP request, and attempts to drop into websocket - connection. - """ - resp = self.http(r) - if resp.status_code == 101: - self.ws_framereader = WebsocketFrameReader( - self.rfile, - self.fp, - self.showresp, - self.hexdump, - self.ws_read_limit, - self.timeout - ) - self.ws_framereader.start() - return resp - - def http(self, r): - """ - Performs a single request. - - r: A language.http.Request object, or a string representing one - request. - - Returns Response if we have a non-ignored response. - - May raise a NetlibException - """ - logger = log.ConnectionLogger( - self.fp, - self.hexdump, - self.rfile if self.showresp else None, - self.wfile if self.showreq else None, - ) - with logger.ctx() as lg: - lg(">> %s" % r) - resp, req = None, None - try: - req = language.serve(r, self.wfile, self.settings) - self.wfile.flush() - - resp = self.protocol.read_response(self.rfile, treq(method=req["method"].encode())) - resp.sslinfo = self.sslinfo - except HttpException as v: - lg("Invalid server response: %s" % v) - raise - except TcpTimeout: - if self.ignoretimeout: - lg("Timeout (ignored)") - return None - lg("Timeout") - raise - finally: - if resp: - lg("<< %s %s: %s bytes" % ( - resp.status_code, utils.xrepr(resp.msg), len(resp.content) - )) - if resp.status_code in self.ignorecodes: - lg.suppress() - return resp - - def request(self, r): - """ - Performs a single request. - - r: A language.message.Messsage object, or a string representing - one. - - Returns Response if we have a non-ignored response. - - May raise a NetlibException - """ - if isinstance(r, basestring): - r = language.parse_pathoc(r, self.use_http2).next() - - if isinstance(r, language.http.Request): - if r.ws: - return self.websocket_start(r) - else: - return self.http(r) - elif isinstance(r, language.websockets.WebsocketFrame): - self.websocket_send_frame(r) - elif isinstance(r, language.http2.Request): - return self.http(r) - # elif isinstance(r, language.http2.Frame): - # TODO: do something - - -def main(args): # pragma: nocover - memo = set([]) - trycount = 0 - p = None - try: - cnt = 0 - while True: - if cnt == args.repeat and args.repeat != 0: - break - if args.wait and cnt != 0: - time.sleep(args.wait) - - cnt += 1 - playlist = itertools.chain(*args.requests) - if args.random: - playlist = random.choice(args.requests) - p = Pathoc( - (args.host, args.port), - ssl=args.ssl, - sni=args.sni, - ssl_version=args.ssl_version, - ssl_options=args.ssl_options, - clientcert=args.clientcert, - ciphers=args.ciphers, - use_http2=args.use_http2, - http2_skip_connection_preface=args.http2_skip_connection_preface, - http2_framedump=args.http2_framedump, - showreq=args.showreq, - showresp=args.showresp, - explain=args.explain, - hexdump=args.hexdump, - ignorecodes=args.ignorecodes, - timeout=args.timeout, - ignoretimeout=args.ignoretimeout, - showsummary=True - ) - trycount = 0 - try: - p.connect(args.connect_to, args.showssl) - except TcpException as v: - print >> sys.stderr, str(v) - continue - except PathocError as v: - print >> sys.stderr, str(v) - sys.exit(1) - for spec in playlist: - if args.explain or args.memo: - spec = spec.freeze(p.settings) - if args.memo: - h = hashlib.sha256(spec.spec()).digest() - if h not in memo: - trycount = 0 - memo.add(h) - else: - trycount += 1 - if trycount > args.memolimit: - print >> sys.stderr, "Memo limit exceeded..." - return - else: - continue - try: - ret = p.request(spec) - if ret and args.oneshot: - return - # We consume the queue when we can, so it doesn't build up. - for i_ in p.wait(timeout=0, finish=False): - pass - except NetlibException: - break - for i_ in p.wait(timeout=0.01, finish=True): - pass - except KeyboardInterrupt: - pass - if p: - p.stop() diff --git a/pathod/pathod/pathoc_cmdline.py b/pathod/pathod/pathoc_cmdline.py deleted file mode 100644 index bf827a9a..00000000 --- a/pathod/pathod/pathoc_cmdline.py +++ /dev/null @@ -1,226 +0,0 @@ -import sys -import argparse -import os -import os.path - -from netlib import tcp -from netlib.http import user_agents -from . import pathoc, version, language - - -def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr): - preparser = argparse.ArgumentParser(add_help=False) - preparser.add_argument( - "--show-uas", dest="showua", action="store_true", default=False, - help="Print user agent shortcuts and exit." - ) - pa = preparser.parse_known_args(argv)[0] - if pa.showua: - print >> stdout, "User agent strings:" - for i in user_agents.UASTRINGS: - print >> stdout, " ", i[1], i[0] - sys.exit(0) - - parser = argparse.ArgumentParser( - description='A perverse HTTP client.', parents=[preparser] - ) - parser.add_argument( - '--version', - action='version', - version="pathoc " + version.VERSION - ) - parser.add_argument( - "-c", dest="connect_to", type=str, default=False, - metavar="HOST:PORT", - help="Issue an HTTP CONNECT to connect to the specified host." - ) - parser.add_argument( - "--memo-limit", dest='memolimit', default=5000, type=int, metavar="N", - help='Stop if we do not find a valid request after N attempts.' - ) - parser.add_argument( - "-m", dest='memo', action="store_true", default=False, - help=""" - Remember specs, and never play the same one twice. Note that this - means requests have to be rendered in memory, which means that - large generated data can cause issues. - """ - ) - parser.add_argument( - "-n", dest='repeat', default=1, type=int, metavar="N", - help='Repeat N times. If 0 repeat for ever.' - ) - parser.add_argument( - "-w", dest='wait', default=0, type=float, metavar="N", - help='Wait N seconds between each request.' - ) - parser.add_argument( - "-r", dest="random", action="store_true", default=False, - help=""" - Select a random request from those specified. If this is not specified, - requests are all played in sequence. - """ - ) - parser.add_argument( - "-t", dest="timeout", type=int, default=None, - help="Connection timeout" - ) - parser.add_argument( - "--http2", dest="use_http2", action="store_true", default=False, - help='Perform all requests over a single HTTP/2 connection.' - ) - parser.add_argument( - "--http2-skip-connection-preface", - dest="http2_skip_connection_preface", - action="store_true", - default=False, - help='Skips the HTTP/2 connection preface before sending requests.') - - parser.add_argument( - 'host', type=str, - metavar="host[:port]", - help='Host and port to connect to' - ) - parser.add_argument( - 'requests', type=str, nargs="+", - help=""" - Request specification, or path to a file containing request - specifcations - """ - ) - - group = parser.add_argument_group( - 'SSL', - ) - group.add_argument( - "-s", dest="ssl", action="store_true", default=False, - help="Connect with SSL" - ) - group.add_argument( - "-C", dest="clientcert", type=str, default=False, - help="Path to a file containing client certificate and private key" - ) - group.add_argument( - "-i", dest="sni", type=str, default=False, - help="SSL Server Name Indication" - ) - group.add_argument( - "--ciphers", dest="ciphers", type=str, default=False, - help="SSL cipher specification" - ) - group.add_argument( - "--ssl-version", dest="ssl_version", type=str, default="secure", - choices=tcp.sslversion_choices.keys(), - help="Set supported SSL/TLS versions. " - "SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+." - ) - - group = parser.add_argument_group( - 'Controlling Output', - """ - Some of these options expand generated values for logging - if - you're generating large data, use them with caution. - """ - ) - group.add_argument( - "-I", dest="ignorecodes", type=str, default="", - help="Comma-separated list of response codes to ignore" - ) - group.add_argument( - "-S", dest="showssl", action="store_true", default=False, - help="Show info on SSL connection" - ) - group.add_argument( - "-e", dest="explain", action="store_true", default=False, - help="Explain requests" - ) - group.add_argument( - "-o", dest="oneshot", action="store_true", default=False, - help="Oneshot - exit after first non-ignored response" - ) - group.add_argument( - "-q", dest="showreq", action="store_true", default=False, - help="Print full request" - ) - group.add_argument( - "-p", dest="showresp", action="store_true", default=False, - help="Print full response" - ) - group.add_argument( - "-T", dest="ignoretimeout", action="store_true", default=False, - help="Ignore timeouts" - ) - group.add_argument( - "-x", dest="hexdump", action="store_true", default=False, - help="Output in hexdump format" - ) - group.add_argument( - "--http2-framedump", dest="http2_framedump", action="store_true", default=False, - help="Output all received & sent HTTP/2 frames" - ) - - args = parser.parse_args(argv[1:]) - - args.ssl_version, args.ssl_options = tcp.sslversion_choices[args.ssl_version] - - args.port = None - if ":" in args.host: - h, p = args.host.rsplit(":", 1) - try: - p = int(p) - except ValueError: - return parser.error("Invalid port in host spec: %s" % args.host) - args.host = h - args.port = p - - if args.port is None: - args.port = 443 if args.ssl else 80 - - try: - args.ignorecodes = [int(i) for i in args.ignorecodes.split(",") if i] - except ValueError: - return parser.error( - "Invalid return code specification: %s" % - args.ignorecodes) - - if args.connect_to: - parts = args.connect_to.split(":") - if len(parts) != 2: - return parser.error( - "Invalid CONNECT specification: %s" % - args.connect_to) - try: - parts[1] = int(parts[1]) - except ValueError: - return parser.error( - "Invalid CONNECT specification: %s" % - args.connect_to) - args.connect_to = parts - else: - args.connect_to = None - - if args.http2_skip_connection_preface: - args.use_http2 = True - - if args.use_http2: - args.ssl = True - - reqs = [] - for r in args.requests: - if os.path.isfile(r): - data = open(r).read() - r = data - try: - reqs.append(language.parse_pathoc(r, args.use_http2)) - except language.ParseException as v: - print >> stderr, "Error parsing request spec: %s" % v.msg - print >> stderr, v.marked() - sys.exit(1) - args.requests = reqs - - return args - - -def go_pathoc(): # pragma: nocover - args = args_pathoc(sys.argv) - pathoc.main(args) diff --git a/pathod/pathod/pathod.py b/pathod/pathod/pathod.py deleted file mode 100644 index 55e75074..00000000 --- a/pathod/pathod/pathod.py +++ /dev/null @@ -1,503 +0,0 @@ -import copy -import logging -import os -import sys -import threading -import urllib - -from netlib import tcp, http, certutils, websockets -from netlib.exceptions import HttpException, HttpReadDisconnect, TcpTimeout, TcpDisconnect, \ - TlsException - -from . import version, app, language, utils, log, protocols -import language.http -import language.actions -import language.exceptions -import language.websockets - - -DEFAULT_CERT_DOMAIN = "pathod.net" -CONFDIR = "~/.mitmproxy" -CERTSTORE_BASENAME = "mitmproxy" -CA_CERT_NAME = "mitmproxy-ca.pem" -DEFAULT_CRAFT_ANCHOR = "/p/" - -logger = logging.getLogger('pathod') - - -class PathodError(Exception): - pass - - -class SSLOptions(object): - def __init__( - self, - confdir=CONFDIR, - cn=None, - sans=(), - not_after_connect=None, - request_client_cert=False, - ssl_version=tcp.SSL_DEFAULT_METHOD, - ssl_options=tcp.SSL_DEFAULT_OPTIONS, - ciphers=None, - certs=None, - alpn_select=b'h2', - ): - self.confdir = confdir - self.cn = cn - self.sans = sans - self.not_after_connect = not_after_connect - self.request_client_cert = request_client_cert - self.ssl_version = ssl_version - self.ssl_options = ssl_options - self.ciphers = ciphers - self.alpn_select = alpn_select - self.certstore = certutils.CertStore.from_store( - os.path.expanduser(confdir), - CERTSTORE_BASENAME - ) - for i in certs or []: - self.certstore.add_cert_file(*i) - - def get_cert(self, name): - if self.cn: - name = self.cn - elif not name: - name = DEFAULT_CERT_DOMAIN - return self.certstore.get_cert(name, self.sans) - - -class PathodHandler(tcp.BaseHandler): - wbufsize = 0 - sni = None - - def __init__( - self, - connection, - address, - server, - logfp, - settings, - http2_framedump=False - ): - tcp.BaseHandler.__init__(self, connection, address, server) - self.logfp = logfp - self.settings = copy.copy(settings) - self.protocol = None - self.use_http2 = False - self.http2_framedump = http2_framedump - - def handle_sni(self, connection): - self.sni = connection.get_servername() - - def http_serve_crafted(self, crafted, logctx): - error, crafted = self.server.check_policy( - crafted, self.settings - ) - if error: - err = self.make_http_error_response(error) - language.serve(err, self.wfile, self.settings) - return None, dict( - type="error", - msg=error - ) - - if self.server.explain and not hasattr(crafted, 'is_error_response'): - crafted = crafted.freeze(self.settings) - logctx(">> Spec: %s" % crafted.spec()) - - response_log = language.serve( - crafted, - self.wfile, - self.settings - ) - if response_log["disconnect"]: - return None, response_log - return self.handle_http_request, response_log - - - def handle_http_request(self, logger): - """ - Returns a (handler, log) tuple. - - handler: Handler for the next request, or None to disconnect - log: A dictionary, or None - """ - with logger.ctx() as lg: - try: - req = self.protocol.read_request(self.rfile) - except HttpReadDisconnect: - return None, None - except HttpException as s: - s = str(s) - lg(s) - return None, dict(type="error", msg=s) - - if req.method == 'CONNECT': - return self.protocol.handle_http_connect([req.host, req.port, req.http_version], lg) - - method = req.method - path = req.path - http_version = req.http_version - headers = req.headers - body = req.content - - clientcert = None - if self.clientcert: - clientcert = dict( - cn=self.clientcert.cn, - subject=self.clientcert.subject, - serial=self.clientcert.serial, - notbefore=self.clientcert.notbefore.isoformat(), - notafter=self.clientcert.notafter.isoformat(), - keyinfo=self.clientcert.keyinfo, - ) - - retlog = dict( - type="crafted", - protocol="http", - request=dict( - path=path, - method=method, - headers=headers.fields, - http_version=http_version, - sni=self.sni, - remote_address=self.address(), - clientcert=clientcert, - ), - cipher=None, - ) - if self.ssl_established: - retlog["cipher"] = self.get_current_cipher() - - m = utils.MemBool() - websocket_key = websockets.WebsocketsProtocol.check_client_handshake(headers) - self.settings.websocket_key = websocket_key - - # If this is a websocket initiation, we respond with a proper - # server response, unless over-ridden. - if websocket_key: - anchor_gen = language.parse_pathod("ws") - else: - anchor_gen = None - - for regex, spec in self.server.anchors: - if regex.match(path): - anchor_gen = language.parse_pathod(spec, self.use_http2) - break - else: - if m(path.startswith(self.server.craftanchor)): - spec = urllib.unquote(path)[len(self.server.craftanchor):] - if spec: - try: - anchor_gen = language.parse_pathod(spec, self.use_http2) - except language.ParseException as v: - lg("Parse error: %s" % v.msg) - anchor_gen = iter([self.make_http_error_response( - "Parse Error", - "Error parsing response spec: %s\n" % ( - v.msg + v.marked() - ) - )]) - else: - if self.use_http2: - anchor_gen = iter([self.make_http_error_response( - "Spec Error", - "HTTP/2 only supports request/response with the craft anchor point: %s" % - self.server.craftanchor - )]) - - if anchor_gen: - spec = anchor_gen.next() - - if self.use_http2 and isinstance(spec, language.http2.Response): - spec.stream_id = req.stream_id - - lg("crafting spec: %s" % spec) - nexthandler, retlog["response"] = self.http_serve_crafted( - spec, - lg - ) - if nexthandler and websocket_key: - self.protocol = protocols.websockets.WebsocketsProtocol(self) - return self.protocol.handle_websocket, retlog - else: - return nexthandler, retlog - else: - return self.protocol.handle_http_app(method, path, headers, body, lg) - - def make_http_error_response(self, reason, body=None): - resp = self.protocol.make_error_response(reason, body) - resp.is_error_response = True - return resp - - def handle(self): - self.settimeout(self.server.timeout) - - if self.server.ssl: - try: - cert, key, _ = self.server.ssloptions.get_cert(None) - self.convert_to_ssl( - cert, - key, - handle_sni=self.handle_sni, - request_client_cert=self.server.ssloptions.request_client_cert, - cipher_list=self.server.ssloptions.ciphers, - method=self.server.ssloptions.ssl_version, - options=self.server.ssloptions.ssl_options, - alpn_select=self.server.ssloptions.alpn_select, - ) - except TlsException as v: - s = str(v) - self.server.add_log( - dict( - type="error", - msg=s - ) - ) - log.write_raw(self.logfp, s) - return - - alp = self.get_alpn_proto_negotiated() - if alp == b'h2': - self.protocol = protocols.http2.HTTP2Protocol(self) - self.use_http2 = True - - if not self.protocol: - self.protocol = protocols.http.HTTPProtocol(self) - - lr = self.rfile if self.server.logreq else None - lw = self.wfile if self.server.logresp else None - logger = log.ConnectionLogger(self.logfp, self.server.hexdump, lr, lw) - - self.settings.protocol = self.protocol - - handler = self.handle_http_request - - while not self.finished: - handler, l = handler(logger) - if l: - self.addlog(l) - if not handler: - return - - def addlog(self, log): - # FIXME: The bytes in the log should not be escaped. We do this at the - # moment because JSON encoding can't handle binary data, and I don't - # want to base64 everything. - if self.server.logreq: - encoded_bytes = self.rfile.get_log().encode("string_escape") - log["request_bytes"] = encoded_bytes - if self.server.logresp: - encoded_bytes = self.wfile.get_log().encode("string_escape") - log["response_bytes"] = encoded_bytes - self.server.add_log(log) - - -class Pathod(tcp.TCPServer): - LOGBUF = 500 - - def __init__( - self, - addr, - ssl=False, - ssloptions=None, - craftanchor=DEFAULT_CRAFT_ANCHOR, - staticdir=None, - anchors=(), - sizelimit=None, - noweb=False, - nocraft=False, - noapi=False, - nohang=False, - timeout=None, - logreq=False, - logresp=False, - explain=False, - hexdump=False, - http2_framedump=False, - webdebug=False, - logfp=sys.stdout, - ): - """ - addr: (address, port) tuple. If port is 0, a free port will be - automatically chosen. - ssloptions: an SSLOptions object. - craftanchor: URL prefix specifying the path under which to anchor - response generation. - staticdir: path to a directory of static resources, or None. - anchors: List of (regex object, language.Request object) tuples, or - None. - sizelimit: Limit size of served data. - nocraft: Disable response crafting. - noapi: Disable the API. - nohang: Disable pauses. - """ - tcp.TCPServer.__init__(self, addr) - self.ssl = ssl - self.ssloptions = ssloptions or SSLOptions() - self.staticdir = staticdir - self.craftanchor = craftanchor - self.sizelimit = sizelimit - self.noweb, self.nocraft = noweb, nocraft - self.noapi, self.nohang = noapi, nohang - self.timeout, self.logreq = timeout, logreq - self.logresp, self.hexdump = logresp, hexdump - self.http2_framedump = http2_framedump - self.explain = explain - self.logfp = logfp - - self.app = app.make_app(noapi, webdebug) - self.app.config["pathod"] = self - self.log = [] - self.logid = 0 - self.anchors = anchors - - self.settings = language.Settings( - staticdir=self.staticdir - ) - - def check_policy(self, req, settings): - """ - A policy check that verifies the request size is within limits. - """ - if self.nocraft: - return "Crafting disabled.", None - try: - req = req.resolve(settings) - l = req.maximum_length(settings) - except language.FileAccessDenied: - return "File access denied.", None - if self.sizelimit and l > self.sizelimit: - return "Response too large.", None - pauses = [isinstance(i, language.actions.PauseAt) for i in req.actions] - if self.nohang and any(pauses): - return "Pauses have been disabled.", None - return None, req - - def handle_client_connection(self, request, client_address): - h = PathodHandler( - request, - client_address, - self, - self.logfp, - self.settings, - self.http2_framedump, - ) - try: - h.handle() - h.finish() - except TcpDisconnect: # pragma: no cover - log.write_raw(self.logfp, "Disconnect") - self.add_log( - dict( - type="error", - msg="Disconnect" - ) - ) - return - except TcpTimeout: - log.write_raw(self.logfp, "Timeout") - self.add_log( - dict( - type="timeout", - ) - ) - return - - def add_log(self, d): - if not self.noapi: - lock = threading.Lock() - with lock: - d["id"] = self.logid - self.log.insert(0, d) - if len(self.log) > self.LOGBUF: - self.log.pop() - self.logid += 1 - return d["id"] - - def clear_log(self): - lock = threading.Lock() - with lock: - self.log = [] - - def log_by_id(self, identifier): - for i in self.log: - if i["id"] == identifier: - return i - - def get_log(self): - return self.log - - -def main(args): # pragma: nocover - ssloptions = SSLOptions( - cn=args.cn, - confdir=args.confdir, - not_after_connect=args.ssl_not_after_connect, - ciphers=args.ciphers, - ssl_version=args.ssl_version, - ssl_options=args.ssl_options, - certs=args.ssl_certs, - sans=args.sans, - ) - - root = logging.getLogger() - if root.handlers: - for handler in root.handlers: - root.removeHandler(handler) - - log = logging.getLogger('pathod') - log.setLevel(logging.DEBUG) - fmt = logging.Formatter( - '%(asctime)s: %(message)s', - datefmt='%d-%m-%y %H:%M:%S', - ) - if args.logfile: - fh = logging.handlers.WatchedFileHandler(args.logfile) - fh.setFormatter(fmt) - log.addHandler(fh) - if not args.daemonize: - sh = logging.StreamHandler() - sh.setFormatter(fmt) - log.addHandler(sh) - - try: - pd = Pathod( - (args.address, args.port), - craftanchor=args.craftanchor, - ssl=args.ssl, - ssloptions=ssloptions, - staticdir=args.staticdir, - anchors=args.anchors, - sizelimit=args.sizelimit, - noweb=args.noweb, - nocraft=args.nocraft, - noapi=args.noapi, - nohang=args.nohang, - timeout=args.timeout, - logreq=args.logreq, - logresp=args.logresp, - hexdump=args.hexdump, - http2_framedump=args.http2_framedump, - explain=args.explain, - webdebug=args.webdebug - ) - except PathodError as v: - print >> sys.stderr, "Error: %s" % v - sys.exit(1) - except language.FileAccessDenied as v: - print >> sys.stderr, "Error: %s" % v - - if args.daemonize: - utils.daemonize() - - try: - print "%s listening on %s:%s" % ( - version.NAMEVERSION, - pd.address.host, - pd.address.port - ) - pd.serve_forever() - except KeyboardInterrupt: - pass diff --git a/pathod/pathod/pathod_cmdline.py b/pathod/pathod/pathod_cmdline.py deleted file mode 100644 index c9272249..00000000 --- a/pathod/pathod/pathod_cmdline.py +++ /dev/null @@ -1,231 +0,0 @@ -import sys -import argparse -import os -import os.path -import re - -from netlib import tcp -from . import pathod, version, utils - - -def args_pathod(argv, stdout_=sys.stdout, stderr_=sys.stderr): - parser = argparse.ArgumentParser( - description='A pathological HTTP/S daemon.' - ) - parser.add_argument( - '--version', - action='version', - version="pathod " + version.VERSION - ) - parser.add_argument( - "-p", - dest='port', - default=9999, - type=int, - help='Port. Specify 0 to pick an arbitrary empty port. (9999)' - ) - parser.add_argument( - "-l", - dest='address', - default="127.0.0.1", - type=str, - help='Listening address. (127.0.0.1)' - ) - parser.add_argument( - "-a", - dest='anchors', - default=[], - type=str, - action="append", - metavar="ANCHOR", - help=""" - Add an anchor. Specified as a string with the form - pattern=spec or pattern=filepath, where pattern is a regular - expression. - """ - ) - parser.add_argument( - "-c", dest='craftanchor', default=pathod.DEFAULT_CRAFT_ANCHOR, type=str, - help=""" - URL path specifying prefix for URL crafting - commands. (%s) - """%pathod.DEFAULT_CRAFT_ANCHOR - ) - parser.add_argument( - "--confdir", - action="store", type = str, dest="confdir", default='~/.mitmproxy', - help = "Configuration directory. (~/.mitmproxy)" - ) - parser.add_argument( - "-d", dest='staticdir', default=None, type=str, - help='Directory for static files.' - ) - parser.add_argument( - "-D", dest='daemonize', default=False, action="store_true", - help='Daemonize.' - ) - parser.add_argument( - "-t", dest="timeout", type=int, default=None, - help="Connection timeout" - ) - parser.add_argument( - "--limit-size", - dest='sizelimit', - default=None, - type=str, - help='Size limit of served responses. Understands size suffixes, i.e. 100k.') - parser.add_argument( - "--noapi", dest='noapi', default=False, action="store_true", - help='Disable API.' - ) - parser.add_argument( - "--nohang", dest='nohang', default=False, action="store_true", - help='Disable pauses during crafted response generation.' - ) - parser.add_argument( - "--noweb", dest='noweb', default=False, action="store_true", - help='Disable both web interface and API.' - ) - parser.add_argument( - "--nocraft", - dest='nocraft', - default=False, - action="store_true", - help='Disable response crafting. If anchors are specified, they still work.') - parser.add_argument( - "--webdebug", dest='webdebug', default=False, action="store_true", - help='Debugging mode for the web app (dev only).' - ) - - group = parser.add_argument_group( - 'SSL', - ) - group.add_argument( - "-s", dest='ssl', default=False, action="store_true", - help='Run in HTTPS mode.' - ) - group.add_argument( - "--cn", - dest="cn", - type=str, - default=None, - help="CN for generated SSL certs. Default: %s" % - pathod.DEFAULT_CERT_DOMAIN) - group.add_argument( - "-C", dest='ssl_not_after_connect', default=False, action="store_true", - help="Don't expect SSL after a CONNECT request." - ) - group.add_argument( - "--cert", dest='ssl_certs', default=[], type=str, - metavar = "SPEC", action="append", - help = """ - Add an SSL certificate. SPEC is of the form "[domain=]path". The domain - may include a wildcard, and is equal to "*" if not specified. The file - at path is a certificate in PEM format. If a private key is included in - the PEM, it is used, else the default key in the conf dir is used. Can - be passed multiple times. - """ - ) - group.add_argument( - "--ciphers", dest="ciphers", type=str, default=False, - help="SSL cipher specification" - ) - group.add_argument( - "--san", dest="sans", type=str, default=[], action="append", - metavar="SAN", - help=""" - Subject Altnernate Name to add to the server certificate. - May be passed multiple times. - """ - ) - group.add_argument( - "--ssl-version", dest="ssl_version", type=str, default="secure", - choices=tcp.sslversion_choices.keys(), - help="Set supported SSL/TLS versions. " - "SSLv2, SSLv3 and 'all' are INSECURE. Defaults to secure, which is TLS1.0+." - ) - - group = parser.add_argument_group( - 'Controlling Logging', - """ - Some of these options expand generated values for logging - if - you're generating large data, use them with caution. - """ - ) - group.add_argument( - "-e", dest="explain", action="store_true", default=False, - help="Explain responses" - ) - group.add_argument( - "-f", dest='logfile', default=None, type=str, - help='Log to file.' - ) - group.add_argument( - "-q", dest="logreq", action="store_true", default=False, - help="Log full request" - ) - group.add_argument( - "-r", dest="logresp", action="store_true", default=False, - help="Log full response" - ) - group.add_argument( - "-x", dest="hexdump", action="store_true", default=False, - help="Log request/response in hexdump format" - ) - group.add_argument( - "--http2-framedump", dest="http2_framedump", action="store_true", default=False, - help="Output all received & sent HTTP/2 frames" - ) - - - args = parser.parse_args(argv[1:]) - - args.ssl_version, args.ssl_options = tcp.sslversion_choices[args.ssl_version] - - certs = [] - for i in args.ssl_certs: - parts = i.split("=", 1) - if len(parts) == 1: - parts = ["*", parts[0]] - parts[1] = os.path.expanduser(parts[1]) - if not os.path.isfile(parts[1]): - return parser.error( - "Certificate file does not exist: %s" % - parts[1]) - certs.append(parts) - args.ssl_certs = certs - - alst = [] - for i in args.anchors: - parts = utils.parse_anchor_spec(i) - if not parts: - return parser.error("Invalid anchor specification: %s" % i) - alst.append(parts) - args.anchors = alst - - sizelimit = None - if args.sizelimit: - try: - sizelimit = utils.parse_size(args.sizelimit) - except ValueError as v: - return parser.error(v) - args.sizelimit = sizelimit - - anchors = [] - for patt, spec in args.anchors: - if os.path.isfile(spec): - data = open(spec).read() - spec = data - try: - arex = re.compile(patt) - except re.error: - return parser.error("Invalid regex in anchor: %s" % patt) - anchors.append((arex, spec)) - args.anchors = anchors - - return args - - -def go_pathod(): # pragma: nocover - args = args_pathod(sys.argv) - pathod.main(args) diff --git a/pathod/pathod/protocols/__init__.py b/pathod/pathod/protocols/__init__.py deleted file mode 100644 index 1a8c7dab..00000000 --- a/pathod/pathod/protocols/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import http, http2, websockets diff --git a/pathod/pathod/protocols/http.py b/pathod/pathod/protocols/http.py deleted file mode 100644 index 1f1765cb..00000000 --- a/pathod/pathod/protocols/http.py +++ /dev/null @@ -1,71 +0,0 @@ -from netlib import tcp, wsgi -from netlib.exceptions import HttpReadDisconnect, TlsException -from netlib.http import http1, Request -from .. import version, language - - -class HTTPProtocol(object): - def __init__(self, pathod_handler): - self.pathod_handler = pathod_handler - - def make_error_response(self, reason, body): - return language.http.make_error_response(reason, body) - - def handle_http_app(self, method, path, headers, body, lg): - """ - Handle a request to the built-in app. - """ - if self.pathod_handler.server.noweb: - crafted = self.pathod_handler.make_http_error_response("Access Denied") - language.serve(crafted, self.pathod_handler.wfile, self.pathod_handler.settings) - return None, dict( - type="error", - msg="Access denied: web interface disabled" - ) - lg("app: %s %s" % (method, path)) - req = wsgi.Request("http", method, path, b"HTTP/1.1", headers, body) - flow = wsgi.Flow(self.pathod_handler.address, req) - sn = self.pathod_handler.connection.getsockname() - a = wsgi.WSGIAdaptor( - self.pathod_handler.server.app, - sn[0], - self.pathod_handler.server.address.port, - version.NAMEVERSION - ) - a.serve(flow, self.pathod_handler.wfile) - return self.pathod_handler.handle_http_request, None - - def handle_http_connect(self, connect, lg): - """ - Handle a CONNECT request. - """ - - self.pathod_handler.wfile.write( - 'HTTP/1.1 200 Connection established\r\n' + - ('Proxy-agent: %s\r\n' % version.NAMEVERSION) + - '\r\n' - ) - self.pathod_handler.wfile.flush() - if not self.pathod_handler.server.ssloptions.not_after_connect: - try: - cert, key, chain_file_ = self.pathod_handler.server.ssloptions.get_cert( - connect[0] - ) - self.pathod_handler.convert_to_ssl( - cert, - key, - handle_sni=self.pathod_handler.handle_sni, - request_client_cert=self.pathod_handler.server.ssloptions.request_client_cert, - cipher_list=self.pathod_handler.server.ssloptions.ciphers, - method=self.pathod_handler.server.ssloptions.ssl_version, - options=self.pathod_handler.server.ssloptions.ssl_options, - alpn_select=self.pathod_handler.server.ssloptions.alpn_select, - ) - except TlsException as v: - s = str(v) - lg(s) - return None, dict(type="error", msg=s) - return self.pathod_handler.handle_http_request, None - - def read_request(self, lg=None): - return http1.read_request(self.pathod_handler.rfile) diff --git a/pathod/pathod/protocols/http2.py b/pathod/pathod/protocols/http2.py deleted file mode 100644 index a098a14e..00000000 --- a/pathod/pathod/protocols/http2.py +++ /dev/null @@ -1,20 +0,0 @@ -from netlib.http import http2 -from .. import version, app, language, utils, log - -class HTTP2Protocol: - - def __init__(self, pathod_handler): - self.pathod_handler = pathod_handler - self.wire_protocol = http2.HTTP2Protocol( - self.pathod_handler, is_server=True, dump_frames=self.pathod_handler.http2_framedump - ) - - def make_error_response(self, reason, body): - return language.http2.make_error_response(reason, body) - - def read_request(self, lg=None): - self.wire_protocol.perform_server_connection_preface() - return self.wire_protocol.read_request(self.pathod_handler.rfile) - - def assemble(self, message): - return self.wire_protocol.assemble(message) diff --git a/pathod/pathod/protocols/websockets.py b/pathod/pathod/protocols/websockets.py deleted file mode 100644 index 134d27bc..00000000 --- a/pathod/pathod/protocols/websockets.py +++ /dev/null @@ -1,56 +0,0 @@ -import time - -from netlib import websockets -from .. import language -from netlib.exceptions import NetlibException - - -class WebsocketsProtocol: - - def __init__(self, pathod_handler): - self.pathod_handler = pathod_handler - - def handle_websocket(self, logger): - while True: - with logger.ctx() as lg: - started = time.time() - try: - frm = websockets.Frame.from_file(self.pathod_handler.rfile) - except NetlibException as e: - lg("Error reading websocket frame: %s" % e) - break - ended = time.time() - lg(frm.human_readable()) - retlog = dict( - type="inbound", - protocol="websockets", - started=started, - duration=ended - started, - frame=dict( - ), - cipher=None, - ) - if self.pathod_handler.ssl_established: - retlog["cipher"] = self.pathod_handler.get_current_cipher() - self.pathod_handler.addlog(retlog) - ld = language.websockets.NESTED_LEADER - if frm.payload.startswith(ld): - nest = frm.payload[len(ld):] - try: - wf_gen = language.parse_websocket_frame(nest) - except language.exceptions.ParseException as v: - logger.write( - "Parse error in reflected frame specifcation:" - " %s" % v.msg - ) - return None, None - for frm in wf_gen: - with logger.ctx() as lg: - frame_log = language.serve( - frm, - self.pathod_handler.wfile, - self.pathod_handler.settings - ) - lg("crafting websocket spec: %s" % frame_log["spec"]) - self.pathod_handler.addlog(frame_log) - return self.handle_websocket, None diff --git a/pathod/pathod/static/bootstrap.min.css b/pathod/pathod/static/bootstrap.min.css deleted file mode 100644 index 2e79d91a..00000000 --- a/pathod/pathod/static/bootstrap.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:32px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:16px;line-height:22px;color:#555;background-color:#fff}a{color:#007fff;text-decoration:none}a:hover,a:focus{color:#06c;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:32px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 11px}.lead{margin-bottom:22px;font-size:24px;font-weight:200;line-height:33px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#dfdfdf}a.muted:hover,a.muted:focus{color:#c6c6c6}.text-warning{color:#fff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-error{color:#fff}a.text-error:hover,a.text-error:focus{color:#e6e6e6}.text-info{color:#fff}a.text-info:hover,a.text-info:focus{color:#e6e6e6}.text-success{color:#fff}a.text-success:hover,a.text-success:focus{color:#e6e6e6}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:11px 0;font-family:inherit;font-weight:300;line-height:22px;color:#080808;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#dfdfdf}h1,h2,h3{line-height:44px}h1{font-size:44px}h2{font-size:36px}h3{font-size:28px}h4{font-size:20px}h5{font-size:16px}h6{font-size:13.6px}h1 small{font-size:28px}h2 small{font-size:20px}h3 small{font-size:16px}h4 small{font-size:16px}.page-header{padding-bottom:10px;margin:44px 0 22px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 11px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:22px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:22px}dt,dd{line-height:22px}dt{font-weight:bold}dd{margin-left:11px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:22px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #dfdfdf}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 22px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:20px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:22px;color:#dfdfdf}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:22px;font-style:normal;line-height:22px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:14px;color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:10.5px;margin:0 0 11px;font-size:15px;line-height:22px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}pre.prettyprint{margin-bottom:22px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 22px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:22px;font-size:24px;line-height:44px;color:#999;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:16.5px;color:#dfdfdf}label,input,button,select,textarea{font-size:16px;font-weight:normal;line-height:22px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:22px;padding:4px 6px;margin-bottom:11px;font-size:16px;line-height:22px;color:#bbb;vertical-align:middle;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #bbb;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:32px;*margin-top:4px;line-height:32px}select{width:220px;background-color:#fff;border:1px solid #bbb}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#dfdfdf;cursor:not-allowed;background-color:#fcfcfc;border-color:#bbb;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#bbb}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#bbb}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#bbb}.radio,.checkbox{min-height:22px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#fff}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#fff}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#fff;background-color:#ff7518;border-color:#fff}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#fff}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#fff}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#fff;background-color:#ff0039;border-color:#fff}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#fff}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#fff}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#fff;background-color:#3fb618;border-color:#fff}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#fff}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#fff}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#fff;background-color:#9954bb;border-color:#fff}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:21px 20px 22px;margin-top:22px;margin-bottom:22px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#7b7b7b}.help-block{display:block;margin-bottom:11px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:11px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:16px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:22px;min-width:16px;padding:4px 5px;font-size:16px;font-weight:normal;line-height:22px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#96ed7a;border-color:#3fb618}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:11px}legend+.control-group{margin-top:22px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:22px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:11px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:22px}.table th,.table td{padding:8px;line-height:22px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:0;border-top-left-radius:0;-moz-border-radius-topleft:0}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:0;border-top-right-radius:0;-moz-border-radius-topright:0}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:0;border-top-left-radius:0;-moz-border-radius-topleft:0}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:0;border-top-right-radius:0;-moz-border-radius-topright:0}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#e8f8fd}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#3fb618}.table tbody tr.error>td{background-color:#ff0039}.table tbody tr.warning>td{background-color:#ff7518}.table tbody tr.info>td{background-color:#9954bb}.table-hover tbody tr.success:hover>td{background-color:#379f15}.table-hover tbody tr.error:hover>td{background-color:#e60033}.table-hover tbody tr.warning:hover>td{background-color:#fe6600}.table-hover tbody tr.info:hover>td{background-color:#8d46b0}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:10px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:22px;color:#999;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#007af5;background-image:-moz-linear-gradient(top,#007fff,#0072e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#007fff),to(#0072e6));background-image:-webkit-linear-gradient(top,#007fff,#0072e6);background-image:-o-linear-gradient(top,#007fff,#0072e6);background-image:linear-gradient(to bottom,#007fff,#0072e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff007fff',endColorstr='#ff0072e6',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#007af5;background-image:-moz-linear-gradient(top,#007fff,#0072e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#007fff),to(#0072e6));background-image:-webkit-linear-gradient(top,#007fff,#0072e6);background-image:-o-linear-gradient(top,#007fff,#0072e6);background-image:linear-gradient(to bottom,#007fff,#0072e6);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff007fff',endColorstr='#ff0072e6',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#dfdfdf}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#eee;border:1px solid #dcdcdc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.well-small{padding:9px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:22px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:16px;line-height:22px;color:#999;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#dfdfdf;*background-color:#c8c8c8;background-image:-moz-linear-gradient(top,#eee,#c8c8c8);background-image:-webkit-gradient(linear,0 0,0 100%,from(#eee),to(#c8c8c8));background-image:-webkit-linear-gradient(top,#eee,#c8c8c8);background-image:-o-linear-gradient(top,#eee,#c8c8c8);background-image:linear-gradient(to bottom,#eee,#c8c8c8);background-repeat:repeat-x;border:1px solid #bbb;*border:0;border-color:#c8c8c8 #c8c8c8 #a2a2a2;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#a2a2a2;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffeeeeee',endColorstr='#ffc8c8c8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#999;background-color:#c8c8c8;*background-color:#bbb}.btn:active,.btn.active{background-color:#aeaeae \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#999;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:22px 30px;font-size:20px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:13.6px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:2px 6px;font-size:12px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0f82f5;*background-color:#0072e6;background-image:-moz-linear-gradient(top,#1a8cff,#0072e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#1a8cff),to(#0072e6));background-image:-webkit-linear-gradient(top,#1a8cff,#0072e6);background-image:-o-linear-gradient(top,#1a8cff,#0072e6);background-image:linear-gradient(to bottom,#1a8cff,#0072e6);background-repeat:repeat-x;border-color:#0072e6 #0072e6 #004c99;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1a8cff',endColorstr='#ff0072e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#0072e6;*background-color:#06c}.btn-primary:active,.btn-primary.active{background-color:#0059b3 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#fe781e;*background-color:#fe6600;background-image:-moz-linear-gradient(top,#ff8432,#fe6600);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ff8432),to(#fe6600));background-image:-webkit-linear-gradient(top,#ff8432,#fe6600);background-image:-o-linear-gradient(top,#ff8432,#fe6600);background-image:linear-gradient(to bottom,#ff8432,#fe6600);background-repeat:repeat-x;border-color:#fe6600 #fe6600 #b14700;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff8432',endColorstr='#fffe6600',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#fe6600;*background-color:#e45c00}.btn-warning:active,.btn-warning.active{background-color:#cb5200 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#f50f43;*background-color:#e60033;background-image:-moz-linear-gradient(top,#ff1a4d,#e60033);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ff1a4d),to(#e60033));background-image:-webkit-linear-gradient(top,#ff1a4d,#e60033);background-image:-o-linear-gradient(top,#ff1a4d,#e60033);background-image:linear-gradient(to bottom,#ff1a4d,#e60033);background-repeat:repeat-x;border-color:#e60033 #e60033 #902;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff1a4d',endColorstr='#ffe60033',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#e60033;*background-color:#cc002e}.btn-danger:active,.btn-danger.active{background-color:#b30028 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#41bb19;*background-color:#379f15;background-image:-moz-linear-gradient(top,#47cd1b,#379f15);background-image:-webkit-gradient(linear,0 0,0 100%,from(#47cd1b),to(#379f15));background-image:-webkit-linear-gradient(top,#47cd1b,#379f15);background-image:-o-linear-gradient(top,#47cd1b,#379f15);background-image:linear-gradient(to bottom,#47cd1b,#379f15);background-repeat:repeat-x;border-color:#379f15 #379f15 #205c0c;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff47cd1b',endColorstr='#ff379f15',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#379f15;*background-color:#2f8912}.btn-success:active,.btn-success.active{background-color:#28720f \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#9b59bb;*background-color:#8d46b0;background-image:-moz-linear-gradient(top,#a466c2,#8d46b0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#a466c2),to(#8d46b0));background-image:-webkit-linear-gradient(top,#a466c2,#8d46b0);background-image:-o-linear-gradient(top,#a466c2,#8d46b0);background-image:linear-gradient(to bottom,#a466c2,#8d46b0);background-repeat:repeat-x;border-color:#8d46b0 #8d46b0 #613079;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffa466c2',endColorstr='#ff8d46b0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#8d46b0;*background-color:#7e3f9d}.btn-info:active,.btn-info.active{background-color:#6f378b \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#080808;*background-color:#000;background-image:-moz-linear-gradient(top,#0d0d0d,#000);background-image:-webkit-gradient(linear,0 0,0 100%,from(#0d0d0d),to(#000));background-image:-webkit-linear-gradient(top,#0d0d0d,#000);background-image:-o-linear-gradient(top,#0d0d0d,#000);background-image:linear-gradient(to bottom,#0d0d0d,#000);background-repeat:repeat-x;border-color:#000 #000 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0d0d0d',endColorstr='#ff000000',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#000;*background-color:#000}.btn-inverse:active,.btn-inverse.active{background-color:#000 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#007fff;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#06c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#999;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:11px;margin-bottom:11px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:16px}.btn-group>.btn-mini{font-size:12px}.btn-group>.btn-small{font-size:13.6px}.btn-group>.btn-large{font-size:20px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-webkit-border-top-left-radius:0;border-top-left-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-topleft:0}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:0;border-top-right-radius:0;-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-topright:0;-moz-border-radius-bottomright:0}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-webkit-border-top-left-radius:0;border-top-left-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-topleft:0}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:0;border-top-right-radius:0;-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-topright:0;-moz-border-radius-bottomright:0}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#c8c8c8}.btn-group.open .btn-primary.dropdown-toggle{background-color:#0072e6}.btn-group.open .btn-warning.dropdown-toggle{background-color:#fe6600}.btn-group.open .btn-danger.dropdown-toggle{background-color:#e60033}.btn-group.open .btn-success.dropdown-toggle{background-color:#379f15}.btn-group.open .btn-info.dropdown-toggle{background-color:#8d46b0}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#000}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.alert{padding:8px 35px 8px 14px;margin-bottom:22px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#ff7518;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.alert,.alert h4{color:#fff}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:22px}.alert-success{color:#fff;background-color:#3fb618;border-color:transparent}.alert-success h4{color:#fff}.alert-danger,.alert-error{color:#fff;background-color:#ff0039;border-color:transparent}.alert-danger h4,.alert-error h4{color:#fff}.alert-info{color:#fff;background-color:#9954bb;border-color:transparent}.alert-info h4{color:#fff}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:22px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:22px;color:#dfdfdf;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#007fff}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:10px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:22px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#bbb;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#007fff}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#007fff;border-bottom-color:#007fff}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#06c;border-bottom-color:#06c}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#bbb;border-bottom-color:#bbb}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#dfdfdf;border-color:#dfdfdf}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#dfdfdf}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#dfdfdf}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:22px;overflow:visible}.navbar-inner{min-height:50px;padding-right:20px;padding-left:20px;background-color:#080808;background-image:-moz-linear-gradient(top,#080808,#080808);background-image:-webkit-gradient(linear,0 0,0 100%,from(#080808),to(#080808));background-image:-webkit-linear-gradient(top,#080808,#080808);background-image:-o-linear-gradient(top,#080808,#080808);background-image:linear-gradient(to bottom,#080808,#080808);background-repeat:repeat-x;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808',endColorstr='#ff080808',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:14px 20px 14px;margin-left:-20px;font-size:20px;font-weight:200;color:#fff;text-shadow:0 1px 0 #080808}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:50px;color:#fff}.navbar-link{color:#fff}.navbar-link:hover,.navbar-link:focus{color:#fff}.navbar .divider-vertical{height:50px;margin:0 9px;border-right:1px solid #080808;border-left:1px solid #080808}.navbar .btn,.navbar .btn-group{margin-top:10px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:10px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:10px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:14px 15px 14px;color:#fff;text-decoration:none;text-shadow:0 1px 0 #080808}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#fff;text-decoration:none;background-color:#3b3b3b}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#fff;text-decoration:none;background-color:transparent;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#000;*background-color:#000;background-image:-moz-linear-gradient(top,#000,#000);background-image:-webkit-gradient(linear,0 0,0 100%,from(#000),to(#000));background-image:-webkit-linear-gradient(top,#000,#000);background-image:-o-linear-gradient(top,#000,#000);background-image:linear-gradient(to bottom,#000,#000);background-repeat:repeat-x;border-color:#000 #000 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff000000',endColorstr='#ff000000',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#000;*background-color:#000}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#000 \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:transparent}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#007fff;background-image:-moz-linear-gradient(top,#007fff,#007fff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#007fff),to(#007fff));background-image:-webkit-linear-gradient(top,#007fff,#007fff);background-image:-o-linear-gradient(top,#007fff,#007fff);background-image:linear-gradient(to bottom,#007fff,#007fff);background-repeat:repeat-x;border-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff007fff',endColorstr='#ff007fff',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#fff}.navbar-inverse .navbar-text{color:#fff}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:rgba(0,0,0,0.05)}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#007fff}.navbar-inverse .navbar-link{color:#fff}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#007fff;border-left-color:#007fff}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#007fff}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#80bfff;border-color:#007fff;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#999}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#999}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#999}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#999;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0072e6;*background-color:#0072e6;background-image:-moz-linear-gradient(top,#0072e6,#0072e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#0072e6),to(#0072e6));background-image:-webkit-linear-gradient(top,#0072e6,#0072e6);background-image:-o-linear-gradient(top,#0072e6,#0072e6);background-image:linear-gradient(to bottom,#0072e6,#0072e6);background-repeat:repeat-x;border-color:#0072e6 #0072e6 #004c99;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0072e6',endColorstr='#ff0072e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#0072e6;*background-color:#06c}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#0059b3 \9}.breadcrumb{padding:8px 15px;margin:0 0 22px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#dfdfdf}.pagination{margin:22px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:22px;text-decoration:none;background-color:#dfdfdf;border:1px solid transparent;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#007fff}.pagination ul>.active>a,.pagination ul>.active>span{color:#dfdfdf;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#dfdfdf;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-webkit-border-top-left-radius:0;border-top-left-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-topleft:0}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:0;border-top-right-radius:0;-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-topright:0;-moz-border-radius-bottomright:0}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:22px 30px;font-size:20px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-webkit-border-top-left-radius:0;border-top-left-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-topleft:0}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:0;border-top-right-radius:0;-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-topright:0;-moz-border-radius-bottomright:0}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-webkit-border-top-left-radius:0;border-top-left-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-topleft:0}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:0;border-top-right-radius:0;-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-topright:0;-moz-border-radius-bottomright:0}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:13.6px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:2px 6px;font-size:12px}.pager{margin:22px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#dfdfdf;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#ff7518;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#ff7518;border-bottom:1px solid #fe6600;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:16px}.popover .arrow:after{border-width:15px;content:""}.popover.top .arrow{bottom:-16px;left:50%;margin-left:-16px;border-top-color:#999;border-top-color:transparent;border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-15px;border-top-color:#ff7518;border-bottom-width:0}.popover.right .arrow{top:50%;left:-16px;margin-top:-16px;border-right-color:#999;border-right-color:transparent;border-left-width:0}.popover.right .arrow:after{bottom:-15px;left:1px;border-right-color:#ff7518;border-left-width:0}.popover.bottom .arrow{top:-16px;left:50%;margin-left:-16px;border-bottom-color:#999;border-bottom-color:transparent;border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-15px;border-bottom-color:#ff7518;border-top-width:0}.popover.left .arrow{top:50%;right:-16px;margin-top:-16px;border-left-color:#999;border-left-color:transparent;border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-15px;border-left-color:#ff7518;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:22px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:22px;border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#007fff;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#bbb}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:13.536px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#dfdfdf}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#fff}.label-important[href],.badge-important[href]{background-color:#e6e6e6}.label-warning,.badge-warning{background-color:#ff7518}.label-warning[href],.badge-warning[href]{background-color:#e45c00}.label-success,.badge-success{background-color:#fff}.label-success[href],.badge-success[href]{background-color:#e6e6e6}.label-info,.badge-info{background-color:#fff}.label-info[href],.badge-info[href]{background-color:#e6e6e6}.label-inverse,.badge-inverse{background-color:#999}.label-inverse[href],.badge-inverse[href]{background-color:#808080}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:22px;margin-bottom:22px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#ff9046;background-image:-moz-linear-gradient(top,#ffa365,#ff7518);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ffa365),to(#ff7518));background-image:-webkit-linear-gradient(top,#ffa365,#ff7518);background-image:-o-linear-gradient(top,#ffa365,#ff7518);background-image:linear-gradient(to bottom,#ffa365,#ff7518);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffa365',endColorstr='#ffff7518',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#ffa365;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:22px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:22px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#080808;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#999;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:22px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:33px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:33px}body{overflow-y:scroll;font-weight:300}h1{font-size:50px}h2,h3{font-size:26px}h4{font-size:14px}h5,h6{font-size:11px}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#999}blockquote{padding:10px 15px;background-color:#eee;border-left-color:#bbb}blockquote.pull-right{padding:10px 15px;border-right-color:#bbb}blockquote small{color:#999}.muted{color:#bbb}.text-warning{color:#ff7518}a.text-warning:hover{color:#e45c00}.text-error{color:#ff0039}a.text-error:hover{color:#cc002e}.text-info{color:#9954bb}a.text-info:hover{color:#7e3f9d}.text-success{color:#3fb618}a.text-success:hover{color:#2f8912}.navbar .navbar-inner{background-image:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.navbar .brand:hover{color:#fff}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{background-color:#3b3b3b;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#fff}.navbar .nav li.dropdown.open>.dropdown-toggle:hover,.navbar .nav li.dropdown.active>.dropdown-toggle:hover,.navbar .nav li.dropdown.open.active>.dropdown-toggle:hover{color:#eee}.navbar .navbar-search .search-query{line-height:normal}.navbar-inverse .brand,.navbar-inverse .nav>li>a{text-shadow:none}.navbar-inverse .brand:hover,.navbar-inverse .nav>.active>a,.navbar-inverse .nav>.active>a:hover,.navbar-inverse .nav>.active>a:focus{color:#fff;background-color:rgba(0,0,0,0.05);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.navbar-inverse .navbar-search .search-query{color:#080808}div.subnav{margin:0 1px;background:#dfdfdf none;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}div.subnav .nav{background-color:transparent}div.subnav .nav>li>a{border-color:transparent}div.subnav .nav>.active>a,div.subnav .nav>.active>a:hover{color:#fff;background-color:#000;border-color:transparent;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}div.subnav-fixed{top:51px;margin:0}.nav .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#007fff}.nav-tabs>li>a{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li>a:hover{color:#fff;background-color:#007fff}.nav-tabs.nav-stacked>.active>a,.nav-tabs.nav-stacked>.active>a:hover{color:#bbb;background-color:#fff}.nav-tabs.nav-stacked>li:first-child>a,.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.tabs-below>.nav-tabs>li>a,.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-pills>li>a{color:#000;background-color:#dfdfdf;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-pills>li>a:hover{color:#fff;background-color:#000}.nav-pills>.disabled>a,.nav-pills>.disabled>a:hover{color:#999;background-color:#eee}.nav-list>li>a{color:#080808}.nav-list>li>a:hover{color:#fff;text-shadow:none;background-color:#007fff}.nav-list .nav-header{font-size:16px;color:#000}.nav-list .divider{background-color:#bbb;border-bottom:0}.pagination ul{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.pagination ul>li>a,.pagination ul>li>span{margin-right:6px;color:#080808}.pagination ul>li>a:hover,.pagination ul>li>span:hover{color:#fff;background-color:#080808}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{margin-right:0}.pagination ul>.active>a,.pagination ul>.active>span{color:#fff}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover{color:#999;background-color:#eee}.pager li>a,.pager li>span{color:#080808;background-color:#dfdfdf;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.pager li>a:hover,.pager li>span:hover{color:#fff;background-color:#080808}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>span{color:#999;background-color:#eee}.breadcrumb{background-color:#dfdfdf}.breadcrumb li{text-shadow:none}.breadcrumb .divider,.breadcrumb .active{color:#080808;text-shadow:none}.btn{padding:5px 12px;color:#080808;text-shadow:none;background-image:none;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn.disabled{box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus{color:#000}.btn-large{padding:22px 30px}.btn-small{padding:2px 10px}.btn-mini{padding:2px 6px}.btn-group>.btn:first-child,.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.dropdown-toggle{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.table tbody tr.success td{color:#fff}.table tbody tr.error td{color:#fff}.table tbody tr.info td{color:#fff}.table-bordered{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child,.table-bordered tfoot:last-child tr:last-child td:first-child{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"]{color:#080808}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#ff7518}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#080808;border-color:#ff7518}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#ff0039}.control-group.error input,.control-group.error select,.control-group.error textarea{color:#080808;border-color:#ff0039}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#3fb618}.control-group.success input,.control-group.success select,.control-group.success textarea{color:#080808;border-color:#3fb618}legend{color:#080808;border-bottom:0}.form-actions{background-color:#eee;border-top:0}.dropdown-menu{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.alert{text-shadow:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.alert-heading,.alert h1,.alert h2,.alert h3,.alert h4,.alert h5,.alert h6{color:#fff}.label{min-width:80px;min-height:80px;font-weight:300;text-shadow:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.label-success{background-color:#3fb618}.label-important{background-color:#ff0039}.label-info{background-color:#9954bb}.label-inverse{background-color:#000}.badge{font-weight:300;text-shadow:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.badge-success{background-color:#3fb618}.badge-important{background-color:#ff0039}.badge-info{background-color:#9954bb}.badge-inverse{background-color:#000}.hero-unit{border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.well{border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}[class^="icon-"],[class*=" icon-"]{margin:0 2px;vertical-align:-2px}a.thumbnail{background-color:#dfdfdf}a.thumbnail:hover{background-color:#bbb;border-color:transparent}.progress{height:6px;background-color:#eee;background-image:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.progress .bar{background-color:#007fff;background-image:none}.progress-info{background-color:#9954bb}.progress-success{background-color:#3fb618}.progress-warning{background-color:#ff7518}.progress-danger{background-color:#ff0039}.modal{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.modal-header{border-bottom:0}.modal-footer{background-color:transparent;border-top:0}.popover{color:#fff;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.popover-title{color:#fff;border-bottom:0}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed}pre{margin-top:10px;color:#333}pre.terminal{font-size:1em;color:#c0c0c0;background:#000}.tlist li{padding-top:.3em;paddint-bottom:.3em} diff --git a/pathod/pathod/static/bootstrap.min.js b/pathod/pathod/static/bootstrap.min.js deleted file mode 100644 index 14356981..00000000 --- a/pathod/pathod/static/bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* Bootstrap.js by @fat & @mdo -* Copyright 2012 Twitter, Inc. -* http://www.apache.org/licenses/LICENSE-2.0.txt -*/ -!function(a){a(function(){"use strict",a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",msTransition:"MSTransitionEnd",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=c,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(a){return a||(this.paused=!0),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this,j=a.Event("slide");this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;if(a.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(j);if(j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})}else{this.$element.trigger(j);if(j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,typeof c=="object"&&c);e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():f.interval&&e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(this.transitioning)return;b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in");if(d&&d.length){e=d.data("collapse");if(e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),this.$element[b](this.$element[0][c])},hide:function(){var b;if(this.transitioning)return;b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a!==null?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c.type=="show"&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c);if(c.isDefaultPrevented())return;this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e,f,g;if(c.is(".disabled, :disabled"))return;return f=c.attr("data-target"),f||(f=c.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,"")),e=a(f),e.length||(e=c.parent()),g=e.hasClass("open"),d(),g||e.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown",".dropdown form",function(a){a.stopPropagation()}).on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('