mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 00:01:36 +00:00
add pathod
This commit is contained in:
commit
175ce43a30
11
pathod/.appveyor.yml
Normal file
11
pathod/.appveyor.yml
Normal file
@ -0,0 +1,11 @@
|
||||
version: '{build}'
|
||||
shallow_clone: true
|
||||
environment:
|
||||
matrix:
|
||||
- PYTHON: "C:\\Python27"
|
||||
install:
|
||||
- "%PYTHON%\\Scripts\\pip install --src . -r requirements.txt"
|
||||
- "%PYTHON%\\python -c \"from OpenSSL import SSL; print(SSL.SSLeay_version(SSL.SSLEAY_VERSION))\""
|
||||
build: off # Not a C# project
|
||||
test_script:
|
||||
- "%PYTHON%\\Scripts\\py.test -n 4"
|
10
pathod/.coveragerc
Normal file
10
pathod/.coveragerc
Normal file
@ -0,0 +1,10 @@
|
||||
[run]
|
||||
branch = True
|
||||
|
||||
[report]
|
||||
show_missing = True
|
||||
include = *libpathod*
|
||||
exclude_lines =
|
||||
pragma: nocover
|
||||
pragma: no cover
|
||||
raise NotImplementedError()
|
6
pathod/.env
Normal file
6
pathod/.env
Normal file
@ -0,0 +1,6 @@
|
||||
DIR="$( dirname "${BASH_SOURCE[0]}" )"
|
||||
ACTIVATE_DIR="$(if [ -f "$DIR/../venv.mitmproxy/bin/activate" ]; then echo 'bin'; else echo 'Scripts'; fi;)"
|
||||
if [ -z "$VIRTUAL_ENV" ] && [ -f "$DIR/../venv.mitmproxy/$ACTIVATE_DIR/activate" ]; then
|
||||
echo "Activating mitmproxy virtualenv..."
|
||||
source "$DIR/../venv.mitmproxy/$ACTIVATE_DIR/activate"
|
||||
fi
|
15
pathod/.gitignore
vendored
Normal file
15
pathod/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Python object files
|
||||
*.py[cd]
|
||||
MANIFEST
|
||||
/build
|
||||
/dist
|
||||
# Vim swap files
|
||||
*.swp
|
||||
/doc
|
||||
.coverage
|
||||
.noseids
|
||||
netlib
|
||||
venv
|
||||
.idea/
|
||||
pathod.egg-info/
|
||||
.cache/
|
22
pathod/.jsbeautifyrc
Normal file
22
pathod/.jsbeautifyrc
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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
|
||||
}
|
16
pathod/.landscape.yml
Normal file
16
pathod/.landscape.yml
Normal file
@ -0,0 +1,16 @@
|
||||
max-line-length: 120
|
||||
pylint:
|
||||
options:
|
||||
dummy-variables-rgx: _$|.+_$|dummy_.+
|
||||
|
||||
disable:
|
||||
- missing-docstring
|
||||
- protected-access
|
||||
- too-few-public-methods
|
||||
- too-many-arguments
|
||||
- too-many-instance-attributes
|
||||
- too-many-locals
|
||||
- too-many-public-methods
|
||||
- too-many-return-statements
|
||||
- too-many-statements
|
||||
- unpacking-non-sequence
|
171
pathod/.sources/bootswatch.less
Normal file
171
pathod/.sources/bootswatch.less
Normal file
@ -0,0 +1,171 @@
|
||||
// Bootswatch.less
|
||||
// Swatch: Journal
|
||||
// Version: 2.0.4
|
||||
// -----------------------------------------------------
|
||||
|
||||
// TYPOGRAPHY
|
||||
// -----------------------------------------------------
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700');
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .navbar .brand {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
// SCAFFOLDING
|
||||
// -----------------------------------------------------
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav a, .navbar .brand, .subnav a, a.btn, .dropdown-menu a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// NAVBAR
|
||||
// -----------------------------------------------------
|
||||
|
||||
.navbar {
|
||||
|
||||
.navbar-inner {
|
||||
@shadow: 0 2px 4px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1);
|
||||
.box-shadow(@shadow);
|
||||
border-top: 1px solid #E5E5E5;
|
||||
.border-radius(0);
|
||||
}
|
||||
|
||||
.brand {
|
||||
text-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #EEEEEE;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-text {
|
||||
line-height: 68px;
|
||||
}
|
||||
|
||||
.nav > li > a {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.border-radius(0);
|
||||
}
|
||||
|
||||
.nav li.dropdown.active > .dropdown-toggle,
|
||||
.nav li.dropdown.active > .dropdown-toggle:hover,
|
||||
.nav li.dropdown.open > .dropdown-toggle,
|
||||
.nav li.dropdown.active.open > .dropdown-toggle,
|
||||
.nav li.dropdown.active.open > .dropdown-toggle:hover {
|
||||
background-color: @grayLighter;
|
||||
color: @linkColor;
|
||||
}
|
||||
|
||||
.nav li.dropdown .dropdown-toggle .caret,
|
||||
.nav .open .caret,
|
||||
.nav .open .dropdown-toggle:hover .caret {
|
||||
border-top-color: @black;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-collapse.in .nav li > a:hover {
|
||||
background-color: @grayLighter;
|
||||
}
|
||||
|
||||
.nav-collapse .nav li > a {
|
||||
color: @textColor;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nav-collapse .navbar-form,
|
||||
.nav-collapse .navbar-search {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.navbar-search .search-query,
|
||||
.navbar-search .search-query:hover {
|
||||
border: 1px solid @grayLighter;
|
||||
color: @textColor;
|
||||
.placeholder(@gray);
|
||||
}
|
||||
}
|
||||
|
||||
div.subnav {
|
||||
background-color: @bodyBackground;
|
||||
background-image: none;
|
||||
@shadow: 0 1px 2px rgba(0,0,0,.25);
|
||||
.box-shadow(@shadow);
|
||||
.border-radius(0);
|
||||
|
||||
&.subnav-fixed {
|
||||
top: @navbarHeight;
|
||||
}
|
||||
|
||||
.nav > li > a:hover,
|
||||
.nav > .active > a,
|
||||
.nav > .active > a:hover {
|
||||
color: @textColor;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nav > li:first-child > a,
|
||||
.nav > li:first-child > a:hover {
|
||||
.border-radius(0);
|
||||
}
|
||||
}
|
||||
|
||||
// BUTTONS
|
||||
// -----------------------------------------------------
|
||||
|
||||
.btn-primary {
|
||||
.buttonBackground(lighten(@linkColor, 5%), @linkColor);
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
// MODALS
|
||||
// -----------------------------------------------------
|
||||
|
||||
.modal {
|
||||
.border-radius(0px);
|
||||
background: @bodyBackground;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.modal-header .close {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background: transparent;
|
||||
.box-shadow(none);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
|
||||
// MISC
|
||||
// -----------------------------------------------------
|
||||
|
||||
code, pre, pre.prettyprint, .well {
|
||||
background-color: @grayLighter;
|
||||
}
|
||||
|
||||
.hero-unit {
|
||||
.box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
|
||||
border: 1px solid rgba(0,0,0,.05);
|
||||
.border-radius(0);
|
||||
}
|
||||
|
||||
.table-bordered, .well, .prettyprint {
|
||||
.border-radius(0);
|
||||
}
|
5
pathod/.sources/make
Executable file
5
pathod/.sources/make
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
pygmentize -f html ../examples/test_context.py > ../libpathod/templates/examples_context.html
|
||||
pygmentize -f html ../examples/test_setup.py > ../libpathod/templates/examples_setup.html
|
||||
pygmentize -f html ../examples/test_setupall.py > ../libpathod/templates/examples_setupall.html
|
||||
pygmentize -f html ../examples/libpathod_pathoc.py > ../libpathod/templates/libpathod_pathoc.html
|
208
pathod/.sources/variables.less
Normal file
208
pathod/.sources/variables.less
Normal file
@ -0,0 +1,208 @@
|
||||
// Variables.less
|
||||
// Variables to customize the look and feel of Bootstrap
|
||||
// Swatch: Journal
|
||||
// Version: 2.0.4
|
||||
// -----------------------------------------------------
|
||||
|
||||
// GLOBAL VALUES
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
// Grays
|
||||
// -------------------------
|
||||
@black: #000;
|
||||
@grayDarker: #222;
|
||||
@grayDark: #333;
|
||||
@gray: #888;
|
||||
@grayLight: #999;
|
||||
@grayLighter: #eee;
|
||||
@white: #fff;
|
||||
|
||||
|
||||
// Accent colors
|
||||
// -------------------------
|
||||
@blue: #4380D3;
|
||||
@blueDark: darken(@blue, 15%);
|
||||
@green: #22B24C;
|
||||
@red: #C00;
|
||||
@yellow: #FCFADB;
|
||||
@orange: #FF7F00;
|
||||
@pink: #CC99CC;
|
||||
@purple: #7a43b6;
|
||||
@tan: #FFCA73;
|
||||
|
||||
|
||||
|
||||
// Scaffolding
|
||||
// -------------------------
|
||||
@bodyBackground: #FCFBFD;
|
||||
@textColor: @grayDarker;
|
||||
|
||||
|
||||
// Links
|
||||
// -------------------------
|
||||
@linkColor: @blue;
|
||||
@linkColorHover: @red;
|
||||
|
||||
|
||||
// Typography
|
||||
// -------------------------
|
||||
@sansFontFamily: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
@serifFontFamily: Georgia, "Times New Roman", Times, serif;
|
||||
@monoFontFamily: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
|
||||
@baseFontSize: 14px;
|
||||
@baseFontFamily: @sansFontFamily;
|
||||
@baseLineHeight: 18px;
|
||||
@altFontFamily: @serifFontFamily;
|
||||
|
||||
@headingsFontFamily: inherit; // empty to use BS default, @baseFontFamily
|
||||
@headingsFontWeight: bold; // instead of browser default, bold
|
||||
@headingsColor: inherit; // empty to use BS default, @textColor
|
||||
|
||||
|
||||
// Tables
|
||||
// -------------------------
|
||||
@tableBackground: transparent; // overall background-color
|
||||
@tableBackgroundAccent: @grayLighter; // for striping
|
||||
@tableBackgroundHover: #f5f5f5; // for hover
|
||||
@tableBorder: #ddd; // table and cell border
|
||||
|
||||
|
||||
// Buttons
|
||||
// -------------------------
|
||||
@btnBackground: @white;
|
||||
@btnBackgroundHighlight: darken(@white, 10%);
|
||||
@btnBorder: darken(@white, 20%);
|
||||
|
||||
@btnPrimaryBackground: @linkColor;
|
||||
@btnPrimaryBackgroundHighlight: spin(@btnPrimaryBackground, 15%);
|
||||
|
||||
@btnInfoBackground: #5bc0de;
|
||||
@btnInfoBackgroundHighlight: #2f96b4;
|
||||
|
||||
@btnSuccessBackground: #62c462;
|
||||
@btnSuccessBackgroundHighlight: #51a351;
|
||||
|
||||
@btnWarningBackground: lighten(@orange, 10%);
|
||||
@btnWarningBackgroundHighlight: @orange;
|
||||
|
||||
@btnDangerBackground: #ee5f5b;
|
||||
@btnDangerBackgroundHighlight: #bd362f;
|
||||
|
||||
@btnInverseBackground: @linkColor;
|
||||
@btnInverseBackgroundHighlight: darken(@linkColor, 5%);
|
||||
|
||||
|
||||
// Forms
|
||||
// -------------------------
|
||||
@inputBackground: @white;
|
||||
@inputBorder: #ccc;
|
||||
@inputBorderRadius: 3px;
|
||||
@inputDisabledBackground: @grayLighter;
|
||||
@formActionsBackground: @grayLighter;
|
||||
|
||||
// Dropdowns
|
||||
// -------------------------
|
||||
@dropdownBackground: @bodyBackground;
|
||||
@dropdownBorder: rgba(0,0,0,.2);
|
||||
@dropdownLinkColor: @textColor;
|
||||
@dropdownLinkColorHover: @textColor;
|
||||
@dropdownLinkBackgroundHover: #eee;
|
||||
@dropdownDividerTop: #e5e5e5;
|
||||
@dropdownDividerBottom: @white;
|
||||
|
||||
|
||||
|
||||
// COMPONENT VARIABLES
|
||||
// --------------------------------------------------
|
||||
|
||||
// Z-index master list
|
||||
// -------------------------
|
||||
// Used for a bird's eye view of components dependent on the z-axis
|
||||
// Try to avoid customizing these :)
|
||||
@zindexDropdown: 1000;
|
||||
@zindexPopover: 1010;
|
||||
@zindexTooltip: 1020;
|
||||
@zindexFixedNavbar: 1030;
|
||||
@zindexModalBackdrop: 1040;
|
||||
@zindexModal: 1050;
|
||||
|
||||
|
||||
// Sprite icons path
|
||||
// -------------------------
|
||||
@iconSpritePath: "../img/glyphicons-halflings.png";
|
||||
@iconWhiteSpritePath: "../img/glyphicons-halflings-white.png";
|
||||
|
||||
|
||||
// Input placeholder text color
|
||||
// -------------------------
|
||||
@placeholderText: @grayLight;
|
||||
|
||||
|
||||
// Hr border color
|
||||
// -------------------------
|
||||
@hrBorder: @grayLighter;
|
||||
|
||||
|
||||
// Navbar
|
||||
// -------------------------
|
||||
@navbarHeight: 50px;
|
||||
@navbarBackground: @bodyBackground;
|
||||
@navbarBackgroundHighlight: @bodyBackground;
|
||||
|
||||
@navbarText: @textColor;
|
||||
@navbarLinkColor: @linkColor;
|
||||
@navbarLinkColorHover: @linkColor;
|
||||
@navbarLinkColorActive: @navbarLinkColorHover;
|
||||
@navbarLinkBackgroundHover: @grayLighter;
|
||||
@navbarLinkBackgroundActive: @grayLighter;
|
||||
|
||||
@navbarSearchBackground: lighten(@navbarBackground, 25%);
|
||||
@navbarSearchBackgroundFocus: @white;
|
||||
@navbarSearchBorder: darken(@navbarSearchBackground, 30%);
|
||||
@navbarSearchPlaceholderColor: #ccc;
|
||||
@navbarBrandColor: @blue;
|
||||
|
||||
|
||||
// Hero unit
|
||||
// -------------------------
|
||||
@heroUnitBackground: @grayLighter;
|
||||
@heroUnitHeadingColor: inherit;
|
||||
@heroUnitLeadColor: inherit;
|
||||
|
||||
|
||||
// Form states and alerts
|
||||
// -------------------------
|
||||
@warningText: #c09853;
|
||||
@warningBackground: #fcf8e3;
|
||||
@warningBorder: darken(spin(@warningBackground, -10), 3%);
|
||||
|
||||
@errorText: #b94a48;
|
||||
@errorBackground: #f2dede;
|
||||
@errorBorder: darken(spin(@errorBackground, -10), 3%);
|
||||
|
||||
@successText: #468847;
|
||||
@successBackground: #dff0d8;
|
||||
@successBorder: darken(spin(@successBackground, -10), 5%);
|
||||
|
||||
@infoText: #3a87ad;
|
||||
@infoBackground: #d9edf7;
|
||||
@infoBorder: darken(spin(@infoBackground, -10), 7%);
|
||||
|
||||
|
||||
|
||||
// GRID
|
||||
// --------------------------------------------------
|
||||
|
||||
// Default 940px grid
|
||||
// -------------------------
|
||||
@gridColumns: 12;
|
||||
@gridColumnWidth: 60px;
|
||||
@gridGutterWidth: 20px;
|
||||
@gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1));
|
||||
|
||||
// Fluid grid
|
||||
// -------------------------
|
||||
@fluidGridColumnWidth: 6.382978723%;
|
||||
@fluidGridGutterWidth: 2.127659574%;
|
70
pathod/.travis.yml
Normal file
70
pathod/.travis.yml
Normal file
@ -0,0 +1,70 @@
|
||||
sudo: false
|
||||
language: python
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: 2.7
|
||||
- python: 2.7
|
||||
env: OPENSSL=1.0.2
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
# Debian sid currently holds OpenSSL 1.0.2
|
||||
# change this with future releases!
|
||||
- debian-sid
|
||||
packages:
|
||||
- libssl-dev
|
||||
- python: pypy
|
||||
- python: pypy
|
||||
env: OPENSSL=1.0.2
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
# Debian sid currently holds OpenSSL 1.0.2
|
||||
# change this with future releases!
|
||||
- debian-sid
|
||||
packages:
|
||||
- libssl-dev
|
||||
allow_failures:
|
||||
# We allow pypy to fail until Travis fixes their infrastructure to a pypy
|
||||
# with a recent enought CFFI library to run cryptography 1.0+.
|
||||
- python: pypy
|
||||
|
||||
install:
|
||||
- "pip install --src . -r requirements.txt"
|
||||
|
||||
before_script:
|
||||
- "openssl version -a"
|
||||
|
||||
script:
|
||||
- "py.test --cov libpathod -v"
|
||||
|
||||
after_success:
|
||||
- coveralls
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.oftc.net#mitmproxy"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
slack:
|
||||
rooms:
|
||||
- mitmproxy:YaDGC9Gt9TEM7o8zkC2OLNsu#ci
|
||||
on_success: always
|
||||
on_failure: always
|
||||
|
||||
# exclude cryptography from cache
|
||||
# it depends on libssl-dev version
|
||||
# which needs to be compiled specifically to each version
|
||||
before_cache:
|
||||
- pip uninstall -y cryptography
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- /home/travis/virtualenv/python2.7.9/lib/python2.7/site-packages
|
||||
- /home/travis/virtualenv/python2.7.9/bin
|
||||
- /home/travis/virtualenv/pypy-2.5.0/site-packages
|
||||
- /home/travis/virtualenv/pypy-2.5.0/bin
|
83
pathod/CHANGELOG
Normal file
83
pathod/CHANGELOG
Normal file
@ -0,0 +1,83 @@
|
||||
7 November 2014: pathod 0.11:
|
||||
|
||||
* Hugely improved SSL support, including dynamic generation of certificates
|
||||
using the mitproxy cacert
|
||||
* pathoc -S dumps information on the remote SSL certificate chain
|
||||
* Big improvements to fuzzing, including random spec selection and memoization to avoid repeating randomly generated patterns
|
||||
* Reflected patterns, allowing you to embed a pathod server response specification in a pathoc request, resolving both on client side. This makes fuzzing proxies and other intermediate systems much better.
|
||||
|
||||
|
||||
25 August 2013: pathod 0.9.2:
|
||||
|
||||
* Adapt to interface changes in netlib
|
||||
|
||||
|
||||
15 May 2013: pathod 0.9 (version synced with mitmproxy):
|
||||
|
||||
* Pathod proxy mode. You can now configure clients to use pathod as an
|
||||
HTTP/S proxy.
|
||||
|
||||
* Pathoc proxy support, including using CONNECT to tunnel directly to
|
||||
targets.
|
||||
|
||||
* Pathoc client certificate support.
|
||||
|
||||
* API improvements, bugfixes.
|
||||
|
||||
|
||||
16 November 2012: pathod 0.3:
|
||||
|
||||
A release focusing on shoring up our fuzzing capabilities, especially with
|
||||
pathoc.
|
||||
|
||||
* pathoc -q and -r options, output full request and response text.
|
||||
|
||||
* pathod -q and -r options, add full request and response text to pathod's
|
||||
log buffer.
|
||||
|
||||
* pathoc and pathod -x option, makes -q and -r options log in hex dump
|
||||
format.
|
||||
|
||||
* pathoc -C option, specify response codes to ignore.
|
||||
|
||||
* pathoc -T option, instructs pathoc to ignore timeouts.
|
||||
|
||||
* pathoc -o option, a one-shot mode that exits after the first non-ignored
|
||||
response.
|
||||
|
||||
* pathoc and pathod -e option, which explains the resulting message by
|
||||
expanding random and generated portions, and logging a reproducible
|
||||
specification.
|
||||
|
||||
* Streamline the specification langauge. HTTP response message is now
|
||||
specified using the "r" mnemonic.
|
||||
|
||||
* Add a "u" mnemonic for specifying User-Agent strings. Add a set of
|
||||
standard user-agent strings accessible through shortcuts.
|
||||
|
||||
* Major internal refactoring and cleanup.
|
||||
|
||||
* Many bugfixes.
|
||||
|
||||
|
||||
22 August 2012: pathod 0.2:
|
||||
|
||||
* Add pathoc, a pathological HTTP client.
|
||||
|
||||
* Add libpathod.test, a truss for using pathod in unit tests.
|
||||
|
||||
* Add an injection operator to the specification language.
|
||||
|
||||
* Allow Python escape sequences in value literals.
|
||||
|
||||
* Allow execution of requests and responses from file, using the new + operator.
|
||||
|
||||
* Add daemonization to Pathod, and make it more robust for public-facing use.
|
||||
|
||||
* Let pathod pick an arbitrary open port if -p 0 is specified.
|
||||
|
||||
* Move from Tornado to netlib, the network library written for mitmproxy.
|
||||
|
||||
* Move the web application to Flask.
|
||||
|
||||
* Massively expand the documentation.
|
6
pathod/CONTRIBUTORS
Normal file
6
pathod/CONTRIBUTORS
Normal file
@ -0,0 +1,6 @@
|
||||
426 Aldo Cortesi
|
||||
74 Maximilian Hils
|
||||
50 Thomas Kriechbaumer
|
||||
1 Felix Yan
|
||||
1 requires.io
|
||||
1 starenka
|
19
pathod/LICENSE
Normal file
19
pathod/LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2008, Aldo Cortesi. All rights reserved.
|
||||
|
||||
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.
|
6
pathod/MANIFEST.in
Normal file
6
pathod/MANIFEST.in
Normal file
@ -0,0 +1,6 @@
|
||||
include LICENSE CHANGELOG README.txt
|
||||
exclude README.mkd
|
||||
recursive-include test *
|
||||
recursive-include libpathod *
|
||||
recursive-include examples *
|
||||
recursive-exclude * *.pyc *.pyo *.swo *.swp
|
44
pathod/README.mkd
Normal file
44
pathod/README.mkd
Normal file
@ -0,0 +1,44 @@
|
||||
[![Build Status](https://img.shields.io/travis/mitmproxy/pathod/master.svg)](https://travis-ci.org/mitmproxy/pathod)
|
||||
[![Code Health](https://landscape.io/github/mitmproxy/pathod/master/landscape.svg?style=flat)](https://landscape.io/github/mitmproxy/pathod/master)
|
||||
[![Coverage Status](https://img.shields.io/coveralls/mitmproxy/pathod/master.svg)](https://coveralls.io/r/mitmproxy/pathod)
|
||||
[![Downloads](https://img.shields.io/pypi/dm/pathod.svg?color=orange)](https://pypi.python.org/pypi/pathod)
|
||||
[![Latest Version](https://img.shields.io/pypi/v/pathod.svg)](https://pypi.python.org/pypi/pathod)
|
||||
[![Supported Python versions](https://img.shields.io/pypi/pyversions/pathod.svg)](https://pypi.python.org/pypi/pathod)
|
||||
|
||||
__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.
|
||||
- __libpathod.test__, an API for easily using __pathod__ and __pathoc__ in unit tests.
|
||||
|
||||
|
||||
# Documentation
|
||||
|
||||
The pathod documentation is self-hosted. Just fire up pathod, like so:
|
||||
|
||||
./pathod
|
||||
|
||||
And then browse to:
|
||||
|
||||
http://localhost:9999
|
||||
|
||||
You can always view the documentation for the latest release at the pathod
|
||||
website:
|
||||
|
||||
http://pathod.net
|
||||
|
||||
# Installing
|
||||
|
||||
If you already have __pip__ on your system, installing __pathod__ and its
|
||||
dependencies is dead simple:
|
||||
|
||||
pip install pathod
|
||||
|
||||
The project has the following dependencies:
|
||||
|
||||
* [netlib](https://github.com/mitmproxy/netlib)
|
||||
* [requests](http://docs.python-requests.org/en/latest/index.html)
|
||||
|
||||
The project's test suite uses the
|
||||
[nose](http://nose.readthedocs.org/en/latest/) unit testing framework.
|
43
pathod/README.txt
Normal file
43
pathod/README.txt
Normal file
@ -0,0 +1,43 @@
|
||||
**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.
|
||||
- **libpathod.test**, an API for easily using pathod and pathoc in unit tests.
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The pathod documentation is self-hosted. Just fire up pathod, like so:
|
||||
|
||||
./pathod
|
||||
|
||||
And then browse to:
|
||||
|
||||
http://localhost:9999
|
||||
|
||||
You can always view the documentation for the latest release at the pathod
|
||||
website:
|
||||
|
||||
http://pathod.net
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
If you already have **pip** on your system, installing **pathod** and its
|
||||
dependencies is dead simple:
|
||||
|
||||
pip install pathod
|
||||
|
||||
The project has the following dependencies:
|
||||
|
||||
* netlib_
|
||||
* requests_
|
||||
|
||||
The project's test suite uses the nose_ unit testing framework.
|
||||
|
||||
.. _netlib: https://github.com/mitmproxy/netlib
|
||||
.. _requests: http://docs.python-requests.org/en/latest/index.html
|
||||
.. _nose: http://nose.readthedocs.org/en/latest/
|
7
pathod/examples/libpathod_pathoc.py
Normal file
7
pathod/examples/libpathod_pathoc.py
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
from libpathod import pathoc
|
||||
|
||||
p = pathoc.Pathoc(("google.com", 80))
|
||||
p.connect()
|
||||
print p.request("get:/")
|
||||
print p.request("get:/foo")
|
23
pathod/examples/test_context.py
Normal file
23
pathod/examples/test_context.py
Normal file
@ -0,0 +1,23 @@
|
||||
import requests
|
||||
from libpathod import test
|
||||
|
||||
|
||||
def test_simple():
|
||||
"""
|
||||
Testing the requests module with
|
||||
a pathod context manager.
|
||||
"""
|
||||
# Start pathod in a separate thread
|
||||
with test.Daemon() as d:
|
||||
# Get a URL for a pathod spec
|
||||
url = d.p("200:b@100")
|
||||
# ... and request it
|
||||
r = requests.put(url)
|
||||
|
||||
# Check the returned data
|
||||
assert r.status_code == 200
|
||||
assert len(r.content) == 100
|
||||
|
||||
# Check pathod's internal log
|
||||
log = d.last_log()["request"]
|
||||
assert log["method"] == "PUT"
|
31
pathod/examples/test_setup.py
Normal file
31
pathod/examples/test_setup.py
Normal file
@ -0,0 +1,31 @@
|
||||
import requests
|
||||
from libpathod import test
|
||||
|
||||
|
||||
class Test:
|
||||
|
||||
"""
|
||||
Testing the requests module with
|
||||
a pathod instance started for
|
||||
each test.
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
self.d = test.Daemon()
|
||||
|
||||
def teardown(self):
|
||||
self.d.shutdown()
|
||||
|
||||
def test_simple(self):
|
||||
# Get a URL for a pathod spec
|
||||
url = self.d.p("200:b@100")
|
||||
# ... and request it
|
||||
r = requests.put(url)
|
||||
|
||||
# Check the returned data
|
||||
assert r.status_code == 200
|
||||
assert len(r.content) == 100
|
||||
|
||||
# Check pathod's internal log
|
||||
log = self.d.last_log()["request"]
|
||||
assert log["method"] == "PUT"
|
39
pathod/examples/test_setupall.py
Normal file
39
pathod/examples/test_setupall.py
Normal file
@ -0,0 +1,39 @@
|
||||
import requests
|
||||
from libpathod import test
|
||||
|
||||
|
||||
class Test:
|
||||
|
||||
"""
|
||||
Testing the requests module with
|
||||
a single pathod instance started
|
||||
for the test suite.
|
||||
"""
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.d = test.Daemon()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
cls.d.shutdown()
|
||||
|
||||
def setup(self):
|
||||
# Clear the pathod logs between tests
|
||||
self.d.clear_log()
|
||||
|
||||
def test_simple(self):
|
||||
# Get a URL for a pathod spec
|
||||
url = self.d.p("200:b@100")
|
||||
# ... and request it
|
||||
r = requests.put(url)
|
||||
|
||||
# Check the returned data
|
||||
assert r.status_code == 200
|
||||
assert len(r.content) == 100
|
||||
|
||||
# Check pathod's internal log
|
||||
log = self.d.last_log()["request"]
|
||||
assert log["method"] == "PUT"
|
||||
|
||||
def test_two(self):
|
||||
assert not self.d.log()
|
0
pathod/libpathod/__init__.py
Normal file
0
pathod/libpathod/__init__.py
Normal file
179
pathod/libpathod/app.py
Normal file
179
pathod/libpathod/app.py
Normal file
@ -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/libpathod')
|
||||
def docs_libpathod():
|
||||
return render(
|
||||
"docs_libpathod.html", True, section="docs", subsection="libpathod"
|
||||
)
|
||||
|
||||
@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/<int:lid>')
|
||||
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
|
113
pathod/libpathod/language/__init__.py
Normal file
113
pathod/libpathod/language/__init__.py
Normal file
@ -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
|
126
pathod/libpathod/language/actions.py
Normal file
126
pathod/libpathod/language/actions.py
Normal file
@ -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))
|
576
pathod/libpathod/language/base.py
Normal file
576
pathod/libpathod/language/base.py
Normal file
@ -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")))
|
22
pathod/libpathod/language/exceptions.py
Normal file
22
pathod/libpathod/language/exceptions.py
Normal file
@ -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)
|
86
pathod/libpathod/language/generators.py
Normal file
86
pathod/libpathod/language/generators.py
Normal file
@ -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
|
381
pathod/libpathod/language/http.py
Normal file
381
pathod/libpathod/language/http.py
Normal file
@ -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)
|
299
pathod/libpathod/language/http2.py
Normal file
299
pathod/libpathod/language/http2.py
Normal file
@ -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:
|
||||
<method>:<path>:<header>:<body>
|
||||
e.g.:
|
||||
GET:/
|
||||
GET:/:h"foo"="bar"
|
||||
POST:/:h"foo"="bar":b'content body payload'
|
||||
|
||||
Normal HTTP responses:
|
||||
<code>:<header>:<body>
|
||||
e.g.:
|
||||
200
|
||||
302:h"foo"="bar"
|
||||
404:h"foo"="bar":b'content body payload'
|
||||
|
||||
Individual HTTP/2 frames:
|
||||
h2f:<payload_length>:<type>:<flags>:<stream_id>:<payload>
|
||||
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
|
96
pathod/libpathod/language/message.py
Normal file
96
pathod/libpathod/language/message.py
Normal file
@ -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()
|
241
pathod/libpathod/language/websockets.py
Normal file
241
pathod/libpathod/language/websockets.py
Normal file
@ -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,
|
||||
)
|
67
pathod/libpathod/language/writer.py
Normal file
67
pathod/libpathod/language/writer.py
Normal file
@ -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
|
83
pathod/libpathod/log.py
Normal file
83
pathod/libpathod/log.py
Normal file
@ -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)
|
534
pathod/libpathod/pathoc.py
Normal file
534
pathod/libpathod/pathoc.py
Normal file
@ -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 OpenSSL._util.lib.Cryptography_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()
|
226
pathod/libpathod/pathoc_cmdline.py
Normal file
226
pathod/libpathod/pathoc_cmdline.py
Normal file
@ -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)
|
503
pathod/libpathod/pathod.py
Normal file
503
pathod/libpathod/pathod.py
Normal file
@ -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
|
231
pathod/libpathod/pathod_cmdline.py
Normal file
231
pathod/libpathod/pathod_cmdline.py
Normal file
@ -0,0 +1,231 @@
|
||||
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)
|
1
pathod/libpathod/protocols/__init__.py
Normal file
1
pathod/libpathod/protocols/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import http, http2, websockets
|
71
pathod/libpathod/protocols/http.py
Normal file
71
pathod/libpathod/protocols/http.py
Normal file
@ -0,0 +1,71 @@
|
||||
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)
|
20
pathod/libpathod/protocols/http2.py
Normal file
20
pathod/libpathod/protocols/http2.py
Normal file
@ -0,0 +1,20 @@
|
||||
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)
|
56
pathod/libpathod/protocols/websockets.py
Normal file
56
pathod/libpathod/protocols/websockets.py
Normal file
@ -0,0 +1,56 @@
|
||||
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
|
9
pathod/libpathod/static/bootstrap.min.css
vendored
Normal file
9
pathod/libpathod/static/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
pathod/libpathod/static/bootstrap.min.js
vendored
Normal file
6
pathod/libpathod/static/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
pathod/libpathod/static/jquery-1.7.2.min.js
vendored
Normal file
4
pathod/libpathod/static/jquery-1.7.2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
pathod/libpathod/static/jquery.localscroll-min.js
vendored
Normal file
9
pathod/libpathod/static/jquery.localscroll-min.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* jQuery.LocalScroll - Animated scrolling navigation, using anchors.
|
||||
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
|
||||
* Dual licensed under MIT and GPL.
|
||||
* Date: 3/11/2009
|
||||
* @author Ariel Flesler
|
||||
* @version 1.2.7
|
||||
**/
|
||||
;(function($){var l=location.href.replace(/#.*/,'');var g=$.localScroll=function(a){$('body').localScroll(a)};g.defaults={duration:1e3,axis:'y',event:'click',stop:true,target:window,reset:true};g.hash=function(a){if(location.hash){a=$.extend({},g.defaults,a);a.hash=false;if(a.reset){var e=a.duration;delete a.duration;$(a.target).scrollTo(0,a);a.duration=e}i(0,location,a)}};$.fn.localScroll=function(b){b=$.extend({},g.defaults,b);return b.lazy?this.bind(b.event,function(a){var e=$([a.target,a.target.parentNode]).filter(d)[0];if(e)i(a,e,b)}):this.find('a,area').filter(d).bind(b.event,function(a){i(a,this,b)}).end().end();function d(){return!!this.href&&!!this.hash&&this.href.replace(this.hash,'')==l&&(!b.filter||$(this).is(b.filter))}};function i(a,e,b){var d=e.hash.slice(1),f=document.getElementById(d)||document.getElementsByName(d)[0];if(!f)return;if(a)a.preventDefault();var h=$(b.target);if(b.lock&&h.is(':animated')||b.onBefore&&b.onBefore.call(b,a,f,h)===false)return;if(b.stop)h.stop(true);if(b.hash){var j=f.id==d?'id':'name',k=$('<a> </a>').attr(j,d).css({position:'absolute',top:$(window).scrollTop(),left:$(window).scrollLeft()});f[j]='';$('body').prepend(k);location=e.hash;k.remove();f[j]=d}h.scrollTo(f,b).trigger('notify.serialScroll',[f])}})(jQuery);
|
11
pathod/libpathod/static/jquery.scrollTo-min.js
vendored
Normal file
11
pathod/libpathod/static/jquery.scrollTo-min.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* jQuery.ScrollTo - Easy element scrolling using jQuery.
|
||||
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
|
||||
* Dual licensed under MIT and GPL.
|
||||
* Date: 3/9/2009
|
||||
* @author Ariel Flesler
|
||||
* @version 1.4.1
|
||||
*
|
||||
* http://flesler.blogspot.com/2007/10/jqueryscrollto.html
|
||||
*/
|
||||
;(function($){var m=$.scrollTo=function(b,h,f){$(window).scrollTo(b,h,f)};m.defaults={axis:'xy',duration:parseFloat($.fn.jquery)>=1.3?0:1};m.window=function(b){return $(window).scrollable()};$.fn.scrollable=function(){return this.map(function(){var b=this,h=!b.nodeName||$.inArray(b.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!h)return b;var f=(b.contentWindow||b).document||b.ownerDocument||b;return $.browser.safari||f.compatMode=='BackCompat'?f.body:f.documentElement})};$.fn.scrollTo=function(l,j,a){if(typeof j=='object'){a=j;j=0}if(typeof a=='function')a={onAfter:a};if(l=='max')l=9e9;a=$.extend({},m.defaults,a);j=j||a.speed||a.duration;a.queue=a.queue&&a.axis.length>1;if(a.queue)j/=2;a.offset=n(a.offset);a.over=n(a.over);return this.scrollable().each(function(){var k=this,o=$(k),d=l,p,g={},q=o.is('html,body');switch(typeof d){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px)?$/.test(d)){d=n(d);break}d=$(d,this);case'object':if(d.is||d.style)p=(d=$(d)).offset()}$.each(a.axis.split(''),function(b,h){var f=h=='x'?'Left':'Top',i=f.toLowerCase(),c='scroll'+f,r=k[c],s=h=='x'?'Width':'Height';if(p){g[c]=p[i]+(q?0:r-o.offset()[i]);if(a.margin){g[c]-=parseInt(d.css('margin'+f))||0;g[c]-=parseInt(d.css('border'+f+'Width'))||0}g[c]+=a.offset[i]||0;if(a.over[i])g[c]+=d[s.toLowerCase()]()*a.over[i]}else g[c]=d[i];if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],u(s));if(!b&&a.queue){if(r!=g[c])t(a.onAfterFirst);delete g[c]}});t(a.onAfter);function t(b){o.animate(g,j,a.easing,b&&function(){b.call(this,l,a)})};function u(b){var h='scroll'+b;if(!q)return k[h];var f='client'+b,i=k.ownerDocument.documentElement,c=k.ownerDocument.body;return Math.max(i[h],c[h])-Math.min(i[f],c[f])}}).end()};function n(b){return typeof b=='object'?b:{top:b,left:b}}})(jQuery);
|
56
pathod/libpathod/static/pathod.css
Normal file
56
pathod/libpathod/static/pathod.css
Normal file
@ -0,0 +1,56 @@
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.innerlink {
|
||||
text-decoration: none;
|
||||
border-bottom:1px dotted;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
padding: 50px 0 60px;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-top: 10px;
|
||||
|
||||
|
||||
}
|
BIN
pathod/libpathod/static/start_quote.png
Normal file
BIN
pathod/libpathod/static/start_quote.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 376 B |
120
pathod/libpathod/static/syntax.css
Normal file
120
pathod/libpathod/static/syntax.css
Normal file
@ -0,0 +1,120 @@
|
||||
.highlight { background: #f8f8f8; }
|
||||
.highlight .c { color: #408080; font-style: italic } /* Comment */
|
||||
.highlight .err { border: 1px solid #FF0000 } /* Error */
|
||||
.highlight .k { color: #008000; font-weight: bold } /* Keyword */
|
||||
.highlight .o { color: #666666 } /* Operator */
|
||||
.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||
.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #FF0000 } /* Generic.Error */
|
||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.highlight .go { color: #808080 } /* Generic.Output */
|
||||
.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #0040D0 } /* Generic.Traceback */
|
||||
.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kp { color: #008000 } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #B00040 } /* Keyword.Type */
|
||||
.highlight .m { color: #666666 } /* Literal.Number */
|
||||
.highlight .s { color: #BA2121 } /* Literal.String */
|
||||
.highlight .na { color: #7D9029 } /* Name.Attribute */
|
||||
.highlight .nb { color: #008000 } /* Name.Builtin */
|
||||
.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #880000 } /* Name.Constant */
|
||||
.highlight .nd { color: #AA22FF } /* Name.Decorator */
|
||||
.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #0000FF } /* Name.Function */
|
||||
.highlight .nl { color: #A0A000 } /* Name.Label */
|
||||
.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #19177C } /* Name.Variable */
|
||||
.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mf { color: #666666 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #666666 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #666666 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #666666 } /* Literal.Number.Oct */
|
||||
.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #BA2121 } /* Literal.String.Double */
|
||||
.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #008000 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #BA2121 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #19177C } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||
.highlight .vc { color: #19177C } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #19177C } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #19177C } /* Name.Variable.Instance */
|
||||
.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||
.grokdoc { background: #f8f8f8; }
|
||||
.grokdoc .c { color: #408080; font-style: italic } /* Comment */
|
||||
.grokdoc .err { border: 1px solid #FF0000 } /* Error */
|
||||
.grokdoc .k { color: #008000; font-weight: bold } /* Keyword */
|
||||
.grokdoc .o { color: #666666 } /* Operator */
|
||||
.grokdoc .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
.grokdoc .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||
.grokdoc .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||
.grokdoc .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
.grokdoc .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.grokdoc .ge { font-style: italic } /* Generic.Emph */
|
||||
.grokdoc .gr { color: #FF0000 } /* Generic.Error */
|
||||
.grokdoc .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.grokdoc .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.grokdoc .go { color: #808080 } /* Generic.Output */
|
||||
.grokdoc .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.grokdoc .gs { font-weight: bold } /* Generic.Strong */
|
||||
.grokdoc .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.grokdoc .gt { color: #0040D0 } /* Generic.Traceback */
|
||||
.grokdoc .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||
.grokdoc .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||
.grokdoc .kp { color: #008000 } /* Keyword.Pseudo */
|
||||
.grokdoc .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||
.grokdoc .kt { color: #B00040 } /* Keyword.Type */
|
||||
.grokdoc .m { color: #666666 } /* Literal.Number */
|
||||
.grokdoc .s { color: #BA2121 } /* Literal.String */
|
||||
.grokdoc .na { color: #7D9029 } /* Name.Attribute */
|
||||
.grokdoc .nb { color: #008000 } /* Name.Builtin */
|
||||
.grokdoc .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||
.grokdoc .no { color: #880000 } /* Name.Constant */
|
||||
.grokdoc .nd { color: #AA22FF } /* Name.Decorator */
|
||||
.grokdoc .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.grokdoc .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
.grokdoc .nf { color: #0000FF } /* Name.Function */
|
||||
.grokdoc .nl { color: #A0A000 } /* Name.Label */
|
||||
.grokdoc .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
.grokdoc .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.grokdoc .nv { color: #19177C } /* Name.Variable */
|
||||
.grokdoc .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
.grokdoc .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.grokdoc .mf { color: #666666 } /* Literal.Number.Float */
|
||||
.grokdoc .mh { color: #666666 } /* Literal.Number.Hex */
|
||||
.grokdoc .mi { color: #666666 } /* Literal.Number.Integer */
|
||||
.grokdoc .mo { color: #666666 } /* Literal.Number.Oct */
|
||||
.grokdoc .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.grokdoc .sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.grokdoc .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||
.grokdoc .s2 { color: #BA2121 } /* Literal.String.Double */
|
||||
.grokdoc .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||
.grokdoc .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||
.grokdoc .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||
.grokdoc .sx { color: #008000 } /* Literal.String.Other */
|
||||
.grokdoc .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||
.grokdoc .s1 { color: #BA2121 } /* Literal.String.Single */
|
||||
.grokdoc .ss { color: #19177C } /* Literal.String.Symbol */
|
||||
.grokdoc .bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||
.grokdoc .vc { color: #19177C } /* Name.Variable.Class */
|
||||
.grokdoc .vg { color: #19177C } /* Name.Variable.Global */
|
||||
.grokdoc .vi { color: #19177C } /* Name.Variable.Instance */
|
||||
.grokdoc .il { color: #666666 } /* Literal.Number.Integer.Long */
|
BIN
pathod/libpathod/static/torture.png
Normal file
BIN
pathod/libpathod/static/torture.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
22
pathod/libpathod/templates/about.html
Normal file
22
pathod/libpathod/templates/about.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "frame.html" %} {% block body %}
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>About</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="span6">
|
||||
<div>
|
||||
<p>pathod is developed by <a href="http://corte.si">Aldo Cortesi</a>.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul>
|
||||
<li>email: <a href="mailto:aldo@corte.si">aldo@corte.si</a></li>
|
||||
<li>twitter: <a href="http://twitter.com/cortesi">@cortesi</a></li>
|
||||
<li>github: <a href="https://github.com/cortesi">github.com/cortesi</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
26
pathod/libpathod/templates/docframe.html
Normal file
26
pathod/libpathod/templates/docframe.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% macro subs(s) %}
|
||||
{% if subsection == s %}
|
||||
class="active"
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="span3">
|
||||
<div class="well sidebar-nav">
|
||||
<ul class="nav nav-list">
|
||||
<li {{subs( "pathod")}}><a href="/docs/pathod">pathod</a></li>
|
||||
<li {{subs( "pathoc")}}><a href="/docs/pathoc">pathoc</a></li>
|
||||
<li {{subs( "lang")}}><a href="/docs/language">language</a></li>
|
||||
<li {{subs( "libpathod")}}><a href="/docs/libpathod">libpathod</a></li>
|
||||
<li {{subs( "test")}}><a href="/docs/test">libpathod.test</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span9">
|
||||
{% block body %} {% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
196
pathod/libpathod/templates/docs_lang.html
Normal file
196
pathod/libpathod/templates/docs_lang.html
Normal file
@ -0,0 +1,196 @@
|
||||
{% extends "docframe.html" %} {% block body %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Language Spec
|
||||
<small>The mini-language at the heart of pathoc and pathod.</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#specifying_requests" data-toggle="tab">HTTP Requests</a></li>
|
||||
<li><a href="#specifying_responses" data-toggle="tab">HTTP Responses</a></li>
|
||||
<li><a href="#websockets" data-toggle="tab">Websocket Frames</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane" id="specifying_responses">
|
||||
{% include "docs_lang_responses.html" %}
|
||||
</div>
|
||||
<div class="tab-pane active" id="specifying_requests">
|
||||
{% include "docs_lang_requests.html" %}
|
||||
</div>
|
||||
<div class="tab-pane" id="websockets">
|
||||
{% include "docs_lang_websockets.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="features">
|
||||
<div class="page-header">
|
||||
<h1>Features</h1>
|
||||
</div>
|
||||
|
||||
<a id="offsetspec"></a>
|
||||
<h2>OFFSET</h2>
|
||||
|
||||
<p>
|
||||
Offsets are calculated relative to the base message, before any injections or other
|
||||
transforms are applied. They have 3 flavors:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>An integer byte offset </li>
|
||||
<li><b>r</b> for a random location</li>
|
||||
<li><b>a</b> for the end of the message</li>
|
||||
</ul>
|
||||
|
||||
<a id="valuespec"></a>
|
||||
<h2>VALUE</h2>
|
||||
|
||||
<h3>Literals</h3>
|
||||
|
||||
<p>Literal values are specified as a quoted strings: </p>
|
||||
|
||||
<pre class="example">"foo"</pre>
|
||||
|
||||
<p>
|
||||
Either single or double quotes are accepted, and quotes can be escaped with backslashes
|
||||
within the string:
|
||||
</p>
|
||||
|
||||
<pre class="example">'fo\'o'</pre>
|
||||
|
||||
<p>Literal values can contain Python-style backslash escape sequences:</p>
|
||||
|
||||
<pre class="example">'foo\r\nbar'</pre>
|
||||
|
||||
<h3>Files</h3>
|
||||
|
||||
<p>
|
||||
You can load a value from a specified file path. To do so, you have to specify a
|
||||
_staticdir_ option to pathod on the command-line, like so:
|
||||
</p>
|
||||
|
||||
<pre class="example">pathod -d ~/myassets</pre>
|
||||
|
||||
<p>
|
||||
All paths are relative paths under this directory. File loads are indicated by starting
|
||||
the value specifier with the left angle bracket:
|
||||
</p>
|
||||
|
||||
<pre class="example"><my/path</pre>
|
||||
|
||||
<p>The path value can also be a quoted string, with the same syntax as literals:</p>
|
||||
|
||||
<pre class="example"><"my/path"</pre>
|
||||
|
||||
|
||||
<h3>Generated values</h3>
|
||||
|
||||
<p>
|
||||
An @-symbol lead-in specifies that generated data should be used. There are two components
|
||||
to a generator specification - a size, and a data type. By default pathod
|
||||
assumes a data type of "bytes".
|
||||
</p>
|
||||
|
||||
<p>Here's a value specifier for generating 100 bytes:
|
||||
|
||||
<pre class="example">@100</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can use standard suffixes to indicate larger values. Here, for instance, is a
|
||||
specifier for generating 100 megabytes:
|
||||
</p>
|
||||
|
||||
<pre class="example">@100m</pre>
|
||||
|
||||
<p>
|
||||
Data is generated and served efficiently - if you really want to send a terabyte
|
||||
of data to a client, pathod can do it. The supported suffixes are:
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>b</td>
|
||||
<td>1024**0 (bytes)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>k</td>
|
||||
<td>1024**1 (kilobytes)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>m</td>
|
||||
<td>1024**2 (megabytes)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>g</td>
|
||||
<td>1024**3 (gigabytes)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>t</td>
|
||||
<td>1024**4 (terabytes)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
Data types are separated from the size specification by a comma. This specification
|
||||
generates 100mb of ASCII:
|
||||
</p>
|
||||
|
||||
<pre class="example">@100m,ascii</pre>
|
||||
|
||||
<p>Supported data types are:</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ascii</td>
|
||||
<td>All ASCII characters</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ascii_letters</td>
|
||||
<td>A-Za-z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ascii_lowercase</td>
|
||||
<td>a-z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ascii_uppercase</td>
|
||||
<td>A-Z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>bytes</td>
|
||||
<td>All 256 byte values</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>digits</td>
|
||||
<td>0-9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>hexdigits</td>
|
||||
<td>0-f</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>octdigits</td>
|
||||
<td>0-7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>punctuation</td>
|
||||
<td>
|
||||
<pre>!"#$%&\'()*+,-./:;
|
||||
<=>?@[\\]^_`{|}~</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whitespace</td>
|
||||
<td>
|
||||
<pre>\t\n\x0b\x0c\r and space</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
114
pathod/libpathod/templates/docs_lang_requests.html
Normal file
114
pathod/libpathod/templates/docs_lang_requests.html
Normal file
@ -0,0 +1,114 @@
|
||||
<pre class="example">method:path:[colon-separated list of features]</pre>
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>method</td>
|
||||
<td>
|
||||
<p>
|
||||
A <a href="#valuespec">VALUE</a> specifying the HTTP method to
|
||||
use. Standard methods do not need to be enclosed in quotes, while
|
||||
non-standard methods can be specified as quoted strings.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The special method <b>ws</b> creates a valid websocket upgrade
|
||||
GET request, and signals to pathoc to switch to websocket recieve
|
||||
mode if the server responds correctly. Apart from that, websocket
|
||||
requests are just like any other, and all aspects of the request
|
||||
can be over-ridden.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>h<a href="#valuespec">VALUE</a>=<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
Set a header.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>r</td>
|
||||
<td>
|
||||
Set the "raw" flag on this response. Pathod will not calculate a Content-Length header
|
||||
if a body is set.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>c<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
A shortcut for setting the Content-Type header. Equivalent to h"Content-Type"=VALUE
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>u<a href="#valuespec">VALUE</a>
|
||||
<br> uSHORTCUT
|
||||
</td>
|
||||
|
||||
<td>
|
||||
Set a User-Agent header on this request. You can specify either a complete
|
||||
<a href="#valuespec">VALUE</a>, or a User-Agent shortcut:
|
||||
|
||||
<table class="table table-condensed">
|
||||
{% for i in uastrings %}
|
||||
<tr>
|
||||
<td><b>{{ i[1] }}</b></td>
|
||||
<td>{{ i[0] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>b<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
Set the body. The appropriate Content-Length header is added automatically unless
|
||||
the "r" flag is set.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>s<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
An embedded Response specification, appended to the path of the request.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>x<a href="#valuespec">INTEGER</a></td>
|
||||
<td>
|
||||
Repeat this message N times.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>d<a href="#offsetspec">OFFSET</a></td>
|
||||
<td>
|
||||
<span class="badge badge-info">HTTP/1 only</span> Disconnect after
|
||||
OFFSET bytes.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>i<a href="#offsetspec">OFFSET</a>,<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
<span class="badge badge-info">HTTP/1 only</span> Inject the specified
|
||||
value at the offset.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>p<a href="#offsetspec">OFFSET</a>,SECONDS</td>
|
||||
<td>
|
||||
<span class="badge badge-info">HTTP/1 only</span> Pause for SECONDS
|
||||
seconds after OFFSET bytes. SECONDS can be an integer or "f" to pause
|
||||
forever.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
88
pathod/libpathod/templates/docs_lang_responses.html
Normal file
88
pathod/libpathod/templates/docs_lang_responses.html
Normal file
@ -0,0 +1,88 @@
|
||||
<pre class="example">code:[colon-separated list of features]</pre>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>code</td>
|
||||
<td>
|
||||
<p>An integer specifying the HTTP response code.</p>
|
||||
<p>
|
||||
The special method <b>ws</b> creates a valid websocket upgrade
|
||||
response (code 101), and moves pathod to websocket mode. Apart
|
||||
from that, websocket responses are just like any other, and all
|
||||
aspects of the response can be over-ridden.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>m<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
<span class="badge badge-info">HTTP/1 only</span> HTTP Reason message.
|
||||
Automatically chosen according to the response code if not specified.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>h<a href="#valuespec">VALUE</a>=<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
Set a header.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>r</td>
|
||||
<td>
|
||||
Set the "raw" flag on this response. Pathod will not calculate a Content-Length header
|
||||
if a body is set, or add a Date header to the response.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>l<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
A shortcut for setting the Location header. Equivalent to h"Location"=VALUE
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>c<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
A shortcut for setting the Content-Type header. Equivalent to h"Content-Type"=VALUE
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>b<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
Set the body. The appropriate Content-Length header is added automatically unless
|
||||
the "r" flag is set.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>d<a href="#offsetspec">OFFSET</a></td>
|
||||
<td>
|
||||
<span class="badge badge-info">HTTP/1 only</span> Disconnect after
|
||||
OFFSET bytes.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>i<a href="#offsetspec">OFFSET</a>,<a href="#valuespec">VALUE</a></td>
|
||||
<td>
|
||||
<span class="badge badge-info">HTTP/1 only</span> Inject the specified
|
||||
value at the offset.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>p<a href="#offsetspec">OFFSET</a>,SECONDS</td>
|
||||
<td>
|
||||
<span class="badge badge-info">HTTP/1 only</span> Pause for SECONDS
|
||||
seconds after OFFSET bytes. SECONDS can be an integer or "f" to pause
|
||||
forever.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
115
pathod/libpathod/templates/docs_lang_websockets.html
Normal file
115
pathod/libpathod/templates/docs_lang_websockets.html
Normal file
@ -0,0 +1,115 @@
|
||||
<pre class="example">wf:[colon-separated list of features]</pre>
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td> b<a href="#valuespec">VALUE</a> </td>
|
||||
<td>
|
||||
Set the frame payload. If a masking key is present, the value is encoded automatically.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> c<a href="#valuespec">INTEGER</a> </td>
|
||||
<td>
|
||||
|
||||
Set the op code. This can either be an integer from 0-15, or be one of the following
|
||||
opcode names: <b>text</b> (the default),
|
||||
<b>continue</b>, <b>binary</b>, <b>close</b>, <b>ping</b>,
|
||||
<b>pong</b>.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> d<a href="#offsetspec">OFFSET</a> </td>
|
||||
<td>
|
||||
Disconnect after OFFSET bytes.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> [-]fin </td>
|
||||
<td>
|
||||
Set or un-set the <b>fin</b> bit.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> i<a href="#offsetspec">OFFSET</a>,<a href="#valuespec">VALUE</a> </td>
|
||||
<td>
|
||||
Inject the specified value at the offset.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> k<a href="#valuespec">VALUE</a> </td>
|
||||
<td>
|
||||
Set the masking key. The resulting value must be exactly 4 bytes long. The special
|
||||
form
|
||||
<b>knone</b> specifies that no key should be set, even if the mask
|
||||
bit is on.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> l<a href="#valuespec">INTEGER</a> </td>
|
||||
<td>
|
||||
Set the payload length in the frame header, regardless of the actual body length.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> [-]mask </td>
|
||||
<td>
|
||||
Set or un-set the <b>mask</b> bit.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> p<a href="#offsetspec">OFFSET</a>,SECONDS </td>
|
||||
<td>
|
||||
Pause for SECONDS seconds after OFFSET bytes. SECONDS can be an integer or "f" to
|
||||
pause forever.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> r<a href="#valuespec">VALUE</a> </td>
|
||||
<td>
|
||||
Set the raw frame payload. This disables masking, even if the key is present.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> [-]rsv1 </td>
|
||||
<td>
|
||||
Set or un-set the <b>rsv1</b> bit.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> [-]rsv2 </td>
|
||||
<td>
|
||||
Set or un-set the <b>rsv2</b> bit.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> [-]rsv3 </td>
|
||||
<td>
|
||||
Set or un-set the <b>rsv3</b> bit.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td> x<a href="#valuespec">INTEGER</a> </td>
|
||||
<td>
|
||||
Repeat this message N times.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
23
pathod/libpathod/templates/docs_libpathod.html
Normal file
23
pathod/libpathod/templates/docs_libpathod.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends "docframe.html" %} {% block body %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
libpathod
|
||||
<small>Using pathod and pathoc in code.</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="span6">
|
||||
<p>
|
||||
Behind the pathod and pathoc command-line tools lurks <b>libpathod</b>,
|
||||
a powerful library for manipulating and serving HTTP requests and responses.
|
||||
The canonical documentation for the library is in the code, and can be
|
||||
accessed using pydoc.
|
||||
</p>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<h1>pathoc</h1>
|
||||
{% include "libpathod_pathoc.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
211
pathod/libpathod/templates/docs_pathoc.html
Normal file
211
pathod/libpathod/templates/docs_pathoc.html
Normal file
@ -0,0 +1,211 @@
|
||||
{% extends "docframe.html" %} {% block body %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
pathoc
|
||||
<small>A perverse HTTP client.</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Pathoc is a perverse HTTP daemon designed to let you craft almost any conceivable
|
||||
HTTP request, including ones that creatively violate the standards. HTTP requests
|
||||
are specified using a
|
||||
<a href="/docs/language">small, terse language</a>, which pathod shares with
|
||||
its server-side twin <a href="/docs/pathod">pathod</a>. To view pathoc's complete
|
||||
range of options, use the command-line help:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">pathoc --help</pre>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Getting Started</h1>
|
||||
</div>
|
||||
|
||||
<p>The basic pattern for pathoc commands is as follows: </p>
|
||||
|
||||
<pre class="terminal">pathoc hostname request [request ...]</pre>
|
||||
|
||||
<p>
|
||||
That is, we specify the hostname to connect to, followed by one or more requests.
|
||||
Lets start with a simple example:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">
|
||||
> pathoc google.com get:/ << 301 Moved Permanently: 219 bytes
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Here, we make a GET request to the path / on port 80 of google.com. Pathoc's output
|
||||
tells us that the server responded with a 301. We can tell pathoc to connect
|
||||
using SSL, in which case the default port is changed to 443 (you can over-ride
|
||||
the default port with the <b>-p</b> command-line option):
|
||||
</p>
|
||||
|
||||
<pre class="terminal">
|
||||
> pathoc -s google.com get:/ << 301 Moved Permanently: 219 bytes
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Multiple Requests</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
There are two ways to tell pathoc to issue multiple requests. The first is to specify
|
||||
them on the command-line, like so:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">
|
||||
> pathoc google.com get:/ get:/ << 301 Moved Permanently: 219 bytes <<
|
||||
301 Moved Permanently: 219 bytes
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
In this case, pathoc issues the specified requests over the same TCP connection -
|
||||
so in the above example only one connection is made to google.com
|
||||
</p>
|
||||
|
||||
<p>The other way to issue multiple requets is to use the <b>-n</b> flag:</p>
|
||||
|
||||
<pre class="terminal">
|
||||
> pathoc -n 2 google.com get:/ << 301 Moved Permanently: 219 bytes << 301
|
||||
Moved Permanently: 219 bytes
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
The output is identical, but two separate TCP connections are made to the upstream
|
||||
server. These two specification styles can be combined:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">
|
||||
> pathoc -n 2 google.com get:/ get:/ << 301 Moved Permanently: 219 bytes <<
|
||||
301 Moved Permanently: 219 bytes << 301 Moved Permanently: 219 bytes <<
|
||||
301 Moved Permanently: 219 bytes
|
||||
</pre>
|
||||
|
||||
<p>Here, two distinct TCP connections are made, with two requests issued over each.</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Basic Fuzzing</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The combination of pathoc's powerful request specification language and a few of
|
||||
its command-line options makes for quite a powerful basic fuzzer. Here's
|
||||
an example:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">
|
||||
> pathoc -e -I 200 -t 2 -n 1000 localhost get:/:b@10:ir,@1
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
The request specified here is a valid GET with a body consisting of 10 random bytes,
|
||||
but with 1 random byte inserted in a random place. This could be in the headers,
|
||||
in the initial request line, or in the body itself. There are a few things
|
||||
to note here:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Corrupting the request in this way will often make the server enter a state where
|
||||
it's awaiting more input from the client. This is where the
|
||||
<b>-t</b> option comes in, which sets a timeout that causes pathoc to
|
||||
disconnect after two seconds.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
The <b>-n</b> option tells pathoc to repeat the request 1000 times.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
The <b>-I</b> option tells pathoc to ignore HTTP 200 response codes.
|
||||
You can use this to fine-tune what pathoc considers to be an exceptional
|
||||
condition, and therefore log-worthy.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
The <b>-e</b> option tells pathoc to print an explanation of each logged
|
||||
request, in the form of an expanded pathoc specification with all random
|
||||
portions and automatic header additions resolved. This lets you precisely
|
||||
replay a request that triggered an error.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Interacting with Proxies</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Pathoc has a reasonably sophisticated suite of features for interacting with proxies.
|
||||
The proxy request syntax very closely mirrors that of straight HTTP, which
|
||||
means that it is possible to make proxy-style requests using pathoc without
|
||||
any additional syntax, by simply specifying a full URL instead of a simple
|
||||
path:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">> pathoc -p 8080 localhost "get:'http://google.com'"</pre>
|
||||
|
||||
<p>
|
||||
Another common use case is to use an HTTP CONNECT request to probe remote servers
|
||||
via a proxy. This is done with the <b>-c</b> command-line option,
|
||||
which allows you to specify a remote host and port pair:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">> pathoc -c google.com:80 -p 8080 localhost get:/</pre>
|
||||
|
||||
<p>
|
||||
Note that pathoc does <b>not</b> negotiate SSL without being explictly instructed
|
||||
to do so. If you're making a CONNECT request to an SSL-protected resource,
|
||||
you must also pass the <b>-s</b> flag:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">> pathoc -sc google.com:443 -p 8080 localhost get:/</pre>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Embedded response specification</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
One interesting feature of the Request sppecification language is that you can embed
|
||||
a response specifcation in it, which is then added to the request path. Here's
|
||||
an example:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">> pathoc localhost:9999 "get:/p/:s'401:ir,@1'"</pre>
|
||||
|
||||
<p>
|
||||
This crafts a request that connects to the pathod server, and which then crafts a
|
||||
response that generates a 401, with one random byte embedded at a random
|
||||
point. The response specification is parsed and expanded by pathoc, so you
|
||||
see syntax errors immediately. This really becomes handy when combined with
|
||||
the <b>-e</b> flag to show the expanded request:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">
|
||||
> > pathoc -e localhost:9999 "get:/p/:s'401:ir,@1'" >> Spec: get:/p/:s'401:i15,\'o\':h\'Content-Length\'=\'0\'':h'Content-Length'='0'
|
||||
<< 401 Unoauthorized: 0 bytes </pre>
|
||||
|
||||
<p>
|
||||
Note that the embedded response has been resolved <i>before</i> being sent
|
||||
to the server, so that "ir,@1" (embed a random byte at a random location)
|
||||
has become "i15,\'o\'" (embed the character "o" at offset 15). You now have
|
||||
a pathoc request specification that is precisely reproducable, even with
|
||||
random components. This feature comes in terribly handy when testing a proxy,
|
||||
since you can now drive the server repsonse completely from the client, and
|
||||
have a complete log of reproducible requests to analyse afterwards.
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
172
pathod/libpathod/templates/docs_pathod.html
Normal file
172
pathod/libpathod/templates/docs_pathod.html
Normal file
@ -0,0 +1,172 @@
|
||||
{% extends "docframe.html" %} {% block body %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
pathod
|
||||
<small>A pathological web daemon.</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Pathod is a pathological HTTP daemon designed to let you craft almost any conceivable
|
||||
HTTP response, including ones that creatively violate the standards. HTTP responses
|
||||
are specified using a
|
||||
<a href="/docs/language">small, terse language</a>, which pathod shares with
|
||||
its evil twin <a href="/docs/pathoc">pathoc</a>.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Getting started</h1>
|
||||
</div>
|
||||
|
||||
<p>To start playing with pathod, simply fire up the daemon:</p>
|
||||
|
||||
<pre class="terminal">./pathod</pre>
|
||||
|
||||
<p>
|
||||
By default, the service listens on port 9999 of localhost. Pathod's documentation
|
||||
is self-hosting, and the pathod daemon exposes an interface that lets you
|
||||
play with the specifciation language, preview what responses and requests
|
||||
would look like on the wire, and view internal logs. To access all of this,
|
||||
just fire up your browser, and point it to the following URL:
|
||||
</p>
|
||||
|
||||
<pre class="example">http://localhost:9999</pre>
|
||||
|
||||
<p>
|
||||
The default crafting anchor point is the path <b>/p/</b>. Anything after
|
||||
this URL prefix is treated as a response specifier. So, hitting the following
|
||||
URL will generate an HTTP 200 response with 100 bytes of random data:
|
||||
</p>
|
||||
|
||||
<pre class="example">http://localhost:9999/p/200:b@100</pre>
|
||||
|
||||
<p>
|
||||
See the <a href="/docs/language">language documentation</a> to get (much)
|
||||
fancier. The pathod daemon also takes a range of configuration options. To
|
||||
view those, use the command-line help:
|
||||
</p>
|
||||
|
||||
<pre class="terminal">./pathod --help</pre>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Acting as a proxy</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Pathod automatically responds to both straight HTTP and proxy requests. For proxy
|
||||
requests, the upstream host is ignored, and the path portion of the URL is
|
||||
used to match anchors. This lets you test software that supports a proxy
|
||||
configuration by spoofing responses from upstream servers.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By default, we treat all proxy CONNECT requests as HTTPS traffic, serving the response
|
||||
using either pathod's built-in certificates, or the cert/key pair specified
|
||||
by the user. You can over-ride this behaviour if you're testing a client
|
||||
that makes a non-SSL CONNECT request using the -C command-line option.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Anchors</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Anchors provide an alternative to specifying the response in the URL. Instead, you
|
||||
attach a response to a pre-configured anchor point, specified with a regex.
|
||||
When a URL matching the regex is requested, the specified response is served.
|
||||
</p>
|
||||
|
||||
<pre class="terminal">./pathod -a "/foo=200"</pre>
|
||||
|
||||
<p>
|
||||
Here, "/foo" is the regex specifying the anchor path, and the part after the "="
|
||||
is a response specifier.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>File Access</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
There are two operators in the <a href="/docs/language">language</a> that
|
||||
load contents from file - the <b>+</b> operator to load an entire request
|
||||
specification from file, and the <b>></b> value specifier. In pathod,
|
||||
both of these operators are restricted to a directory specified at startup,
|
||||
or disabled if no directory is specified:</p>
|
||||
<pre class="terminal">./pathod -d ~/staticdir"</pre>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Internal Error Responses</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Pathod uses the non-standard 800 response code to indicate internal errors, to distinguish
|
||||
them from crafted responses. For example, a request to:
|
||||
</p>
|
||||
|
||||
<pre class="example">http://localhost:9999/p/foo</pre>
|
||||
|
||||
<p>
|
||||
... will return an 800 response because "foo" is not a valid page specifier.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>API</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
pathod exposes a simple API, intended to make it possible to drive and inspect the
|
||||
daemon remotely for use in unit testing and the like.
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
/api/clear_log
|
||||
</td>
|
||||
<td>
|
||||
A POST to this URL clears the log buffer.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
/api/info
|
||||
</td>
|
||||
<td>
|
||||
Basic version and configuration info.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
/api/log
|
||||
</td>
|
||||
<td>
|
||||
Returns the current log buffer. At the moment the buffer size is 500 entries - when
|
||||
the log grows larger than this, older entries are discarded.
|
||||
The returned data is a JSON dictionary, with the form:
|
||||
|
||||
<pre>{ 'log': [ ENTRIES ] } </pre> You can preview the JSON data
|
||||
returned for a log entry through the built-in web interface.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
50
pathod/libpathod/templates/docs_test.html
Normal file
50
pathod/libpathod/templates/docs_test.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends "docframe.html" %} {% block body %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
libpathod.test
|
||||
<small>Using libpathod in unit tests.</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p>The <b>libpathod.test</b> module is a light, flexible testing layer for HTTP clients.
|
||||
It works by firing up a Pathod instance in a separate thread, letting you use
|
||||
Pathod's full abilities to generate responses, and then query Pathod's internal
|
||||
logs to establish what happened. All the mechanics of startup, shutdown, finding
|
||||
free ports and so forth are taken care of for you.
|
||||
</p>
|
||||
|
||||
<p>The canonical docs can be accessed using pydoc: </p>
|
||||
|
||||
<pre class="terminal">pydoc libpathod.test</pre>
|
||||
|
||||
<p>
|
||||
The remainder of this page demonstrates some common interaction patterns using
|
||||
<a href="http://nose.readthedocs.org/en/latest/">nose</a>. These examples are
|
||||
also applicable with only minor modification to most commonly used Python testing
|
||||
engines.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Context Manager</h1>
|
||||
</div>
|
||||
|
||||
{% include "examples_context.html" %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>One instance per test</h1>
|
||||
</div>
|
||||
|
||||
{% include "examples_setup.html" %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>One instance per suite</h1>
|
||||
</div>
|
||||
|
||||
{% include "examples_setupall.html" %}
|
||||
</section>
|
||||
{% endblock %}
|
39
pathod/libpathod/templates/download.html
Normal file
39
pathod/libpathod/templates/download.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "frame.html" %} {% block body %}
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>pip</h1>
|
||||
</div>
|
||||
|
||||
<p>The easiest way to install pathod is to use pip:</p>
|
||||
|
||||
<pre>pip install pathod</pre>
|
||||
|
||||
<p>
|
||||
This will automatically pull in all the dependencies, and you should be good to go.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>github</h1>
|
||||
</div>
|
||||
|
||||
<p>You can find the project source on GitHub:</p>
|
||||
|
||||
<div style="margin-top: 20px; margin-bottom: 20px">
|
||||
<a class="btn btn-primary btn-large" href="https://github.com/mitmproxy/pathod">github.com/mitmproxy/pathod</a>
|
||||
</div>
|
||||
|
||||
<p>Please also use the <a href="https://github.com/mitmproxy/pathod/issues">github issue tracker</a> to report bugs.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>tarball</h1>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; margin-bottom: 20px">
|
||||
<a class="btn btn-primary btn-large" href="https://github.com/downloads/mitmproxy/pathod/pathod-{{version}}.tar.gz">pathod-{{version}}.tar.gz</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
24
pathod/libpathod/templates/examples_context.html
Normal file
24
pathod/libpathod/templates/examples_context.html
Normal file
@ -0,0 +1,24 @@
|
||||
<div class="highlight"><pre><span class="kn">import</span> <span class="nn">requests</span>
|
||||
<span class="kn">from</span> <span class="nn">libpathod</span> <span class="kn">import</span> <span class="n">test</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">test_simple</span><span class="p">():</span>
|
||||
<span class="sd">"""</span>
|
||||
<span class="sd"> Testing the requests module with</span>
|
||||
<span class="sd"> a pathod context manager.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="c"># Start pathod in a separate thread</span>
|
||||
<span class="k">with</span> <span class="n">test</span><span class="o">.</span><span class="n">Daemon</span><span class="p">()</span> <span class="k">as</span> <span class="n">d</span><span class="p">:</span>
|
||||
<span class="c"># Get a URL for a pathod spec</span>
|
||||
<span class="n">url</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s">"200:b@100"</span><span class="p">)</span>
|
||||
<span class="c"># ... and request it</span>
|
||||
<span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
|
||||
|
||||
<span class="c"># Check the returned data</span>
|
||||
<span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
|
||||
<span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">content</span><span class="p">)</span> <span class="o">==</span> <span class="mi">100</span>
|
||||
|
||||
<span class="c"># Check pathod's internal log</span>
|
||||
<span class="n">log</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="n">last_log</span><span class="p">()[</span><span class="s">"request"</span><span class="p">]</span>
|
||||
<span class="k">assert</span> <span class="n">log</span><span class="p">[</span><span class="s">"method"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"PUT"</span>
|
||||
</pre></div>
|
32
pathod/libpathod/templates/examples_setup.html
Normal file
32
pathod/libpathod/templates/examples_setup.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div class="highlight"><pre><span class="kn">import</span> <span class="nn">requests</span>
|
||||
<span class="kn">from</span> <span class="nn">libpathod</span> <span class="kn">import</span> <span class="n">test</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">Test</span><span class="p">:</span>
|
||||
|
||||
<span class="sd">"""</span>
|
||||
<span class="sd"> Testing the requests module with</span>
|
||||
<span class="sd"> a pathod instance started for</span>
|
||||
<span class="sd"> each test.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">setUp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">d</span> <span class="o">=</span> <span class="n">test</span><span class="o">.</span><span class="n">Daemon</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">tearDown</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">shutdown</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">test_simple</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="c"># Get a URL for a pathod spec</span>
|
||||
<span class="n">url</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s">"200:b@100"</span><span class="p">)</span>
|
||||
<span class="c"># ... and request it</span>
|
||||
<span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
|
||||
|
||||
<span class="c"># Check the returned data</span>
|
||||
<span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
|
||||
<span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">content</span><span class="p">)</span> <span class="o">==</span> <span class="mi">100</span>
|
||||
|
||||
<span class="c"># Check pathod's internal log</span>
|
||||
<span class="n">log</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">last_log</span><span class="p">()[</span><span class="s">"request"</span><span class="p">]</span>
|
||||
<span class="k">assert</span> <span class="n">log</span><span class="p">[</span><span class="s">"method"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"PUT"</span>
|
||||
</pre></div>
|
40
pathod/libpathod/templates/examples_setupall.html
Normal file
40
pathod/libpathod/templates/examples_setupall.html
Normal file
@ -0,0 +1,40 @@
|
||||
<div class="highlight"><pre><span class="kn">import</span> <span class="nn">requests</span>
|
||||
<span class="kn">from</span> <span class="nn">libpathod</span> <span class="kn">import</span> <span class="n">test</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">Test</span><span class="p">:</span>
|
||||
|
||||
<span class="sd">"""</span>
|
||||
<span class="sd"> Testing the requests module with</span>
|
||||
<span class="sd"> a single pathod instance started</span>
|
||||
<span class="sd"> for the test suite.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="nd">@classmethod</span>
|
||||
<span class="k">def</span> <span class="nf">setUpAll</span><span class="p">(</span><span class="n">cls</span><span class="p">):</span>
|
||||
<span class="n">cls</span><span class="o">.</span><span class="n">d</span> <span class="o">=</span> <span class="n">test</span><span class="o">.</span><span class="n">Daemon</span><span class="p">()</span>
|
||||
|
||||
<span class="nd">@classmethod</span>
|
||||
<span class="k">def</span> <span class="nf">tearDownAll</span><span class="p">(</span><span class="n">cls</span><span class="p">):</span>
|
||||
<span class="n">cls</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">shutdown</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">setUp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="c"># Clear the pathod logs between tests</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">clear_log</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">test_simple</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="c"># Get a URL for a pathod spec</span>
|
||||
<span class="n">url</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s">"200:b@100"</span><span class="p">)</span>
|
||||
<span class="c"># ... and request it</span>
|
||||
<span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
|
||||
|
||||
<span class="c"># Check the returned data</span>
|
||||
<span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
|
||||
<span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">content</span><span class="p">)</span> <span class="o">==</span> <span class="mi">100</span>
|
||||
|
||||
<span class="c"># Check pathod's internal log</span>
|
||||
<span class="n">log</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">last_log</span><span class="p">()[</span><span class="s">"request"</span><span class="p">]</span>
|
||||
<span class="k">assert</span> <span class="n">log</span><span class="p">[</span><span class="s">"method"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"PUT"</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">test_two</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">assert</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">d</span><span class="o">.</span><span class="n">log</span><span class="p">()</span>
|
||||
</pre></div>
|
7
pathod/libpathod/templates/frame.html
Normal file
7
pathod/libpathod/templates/frame.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends "layout.html" %} {% block content %}
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
{% block body %} {% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
60
pathod/libpathod/templates/index.html
Normal file
60
pathod/libpathod/templates/index.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends "frame.html" %} {% block body %}
|
||||
<div class="masthead">
|
||||
<div class="container">
|
||||
<h1>pathod: pathological HTTP</h1>
|
||||
|
||||
<p>Crafted malice for tormenting HTTP clients and servers</p>
|
||||
|
||||
<img src="/static/torture.png">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="span6">
|
||||
<div>
|
||||
<h2><a href="/docs/pathod">pathod</a></h2>
|
||||
|
||||
<p>A pathological web daemon.</p>
|
||||
|
||||
{% include "response_previewform.html" %}
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="span6">
|
||||
<div>
|
||||
<h2><a href="/docs/pathoc">pathoc</a></h2>
|
||||
|
||||
<p>A perverse HTTP client.</p>
|
||||
|
||||
{% include "request_previewform.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<h1>Install</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="span6">
|
||||
<div>
|
||||
<h2>pip</h2>
|
||||
|
||||
<pre>pip install pathod</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="span6">
|
||||
<div>
|
||||
<h2>source</h2>
|
||||
|
||||
<ul>
|
||||
<li>Current release: <a href="http://mitmproxy.org/download/pathod-{{version}}.tar.gz">pathod {{version}}</a></li>
|
||||
<li>GitHub: <a href="https://github.com/mitmproxy/pathod">github.com/mitmproxy/pathod</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
75
pathod/libpathod/templates/layout.html
Normal file
75
pathod/libpathod/templates/layout.html
Normal file
@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>pathod</title>
|
||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/pathod.css" rel="stylesheet">
|
||||
<link href="/static/syntax.css" rel="stylesheet">
|
||||
<script src="/static/jquery-1.7.2.min.js"></script>
|
||||
<script src="/static/jquery.scrollTo-min.js"></script>
|
||||
<script src="/static/jquery.localscroll-min.js"></script>
|
||||
<script src="/static/bootstrap.min.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-top: 60px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</a>
|
||||
<a class="brand" href="/index.html">pathod</a>
|
||||
<div class="nav-collapse">
|
||||
<ul class="nav">
|
||||
<li {% if section=="main" %} class="active" {% endif %}><a href="/">home</a></li>
|
||||
{% if not noapi %}
|
||||
<li {% if section=="log" %} class="active" {% endif %}><a href="/log">log</a></li>
|
||||
{% endif %}
|
||||
<li {% if section=="docs" %} class="active" {% endif %}><a href="/docs/pathod">docs</a></li>
|
||||
<li {% if section=="about" %} class="active" {% endif %}><a href="/about">about</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% block content %} {% endblock %}
|
||||
<hr>
|
||||
<footer>
|
||||
<span>© Aldo Cortesi 2015</span>
|
||||
<span class="pull-right">[served with pathod]</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
$(function() {
|
||||
$.localScroll({
|
||||
duration: 300,
|
||||
offset: {
|
||||
top: -45
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
8
pathod/libpathod/templates/libpathod_pathoc.html
Normal file
8
pathod/libpathod/templates/libpathod_pathoc.html
Normal file
@ -0,0 +1,8 @@
|
||||
<div class="highlight"><pre><span class="c">#!/usr/bin/env python</span>
|
||||
<span class="kn">from</span> <span class="nn">libpathod</span> <span class="kn">import</span> <span class="n">pathoc</span>
|
||||
|
||||
<span class="n">p</span> <span class="o">=</span> <span class="n">pathoc</span><span class="o">.</span><span class="n">Pathoc</span><span class="p">((</span><span class="s">"google.com"</span><span class="p">,</span> <span class="mi">80</span><span class="p">))</span>
|
||||
<span class="n">p</span><span class="o">.</span><span class="n">connect</span><span class="p">()</span>
|
||||
<span class="k">print</span> <span class="n">p</span><span class="o">.</span><span class="n">request</span><span class="p">(</span><span class="s">"get:/"</span><span class="p">)</span>
|
||||
<span class="k">print</span> <span class="n">p</span><span class="o">.</span><span class="n">request</span><span class="p">(</span><span class="s">"get:/foo"</span><span class="p">)</span>
|
||||
</pre></div>
|
31
pathod/libpathod/templates/log.html
Normal file
31
pathod/libpathod/templates/log.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "frame.html" %} {% block body %}
|
||||
<form style="float: right" method="POST" action="/log/clear">
|
||||
<button type="submit" class="btn">clear</button>
|
||||
</form>
|
||||
|
||||
<h1>Logs</h1>
|
||||
<hr>
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>method</th>
|
||||
<th>path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in log %}
|
||||
<tr>
|
||||
{% if i["type"] == 'error' %}
|
||||
<td colspan="3">ERROR: {{ i["msg"] }}</td>
|
||||
{% else %}
|
||||
<td>{{ i["id"] }}</td>
|
||||
<td>{{ i["request"]["method"] }}</td>
|
||||
<td><a href="/log/{{ i[" id "] }}">{{ i["request"]["path"] }}</a></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
8
pathod/libpathod/templates/onelog.html
Normal file
8
pathod/libpathod/templates/onelog.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "frame.html" %} {% block body %}
|
||||
<h2>Log entry {{ lid }}</h2>
|
||||
<hr>
|
||||
|
||||
<pre>
|
||||
{{ alog }}
|
||||
</pre>
|
||||
{% endblock %}
|
44
pathod/libpathod/templates/request_preview.html
Normal file
44
pathod/libpathod/templates/request_preview.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends "frame.html" %} {% block body %}
|
||||
<div class="page-header">
|
||||
<h1>pathoc preview</h1>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px" class="row">
|
||||
<div class="span2 header">
|
||||
Specification:
|
||||
</div>
|
||||
<div class="span10">
|
||||
{% include "request_previewform.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if syntaxerror %}
|
||||
<div class="row">
|
||||
<div class="span2 header">
|
||||
Error:
|
||||
</div>
|
||||
<div class="span10">
|
||||
<p style="color: #ff0000">{{ syntaxerror }}</p>
|
||||
<pre>{{ marked }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% elif error %}
|
||||
<div class="row">
|
||||
<div class="span2 header">
|
||||
Error:
|
||||
</div>
|
||||
<div class="span10">
|
||||
<p style="color: #ff0000">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="span2 header">
|
||||
Request:
|
||||
</div>
|
||||
<div class="span10">
|
||||
<pre>{{ output }}</pre>
|
||||
<p>Note: pauses are skipped when generating previews!</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endblock %}
|
53
pathod/libpathod/templates/request_previewform.html
Normal file
53
pathod/libpathod/templates/request_previewform.html
Normal file
@ -0,0 +1,53 @@
|
||||
<form style="margin-bottom: 0" class="form-inline" method="GET" action="/request_preview">
|
||||
<input style="width: 18em" id="spec" name="spec" class="input-medium" value="{{spec}}"
|
||||
placeholder="method:path:[features]">
|
||||
<input type="submit" class="btn" value="preview">
|
||||
</form>
|
||||
|
||||
<a class="innerlink" data-toggle="collapse" data-target="#requestexamples">examples</a>
|
||||
|
||||
<div id="requestexamples" class="collapse">
|
||||
<p>
|
||||
Check out the <a href="/docs/language">complete language docs</a>. Here are
|
||||
some examples to get you started:
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="/request_preview?spec=get:/">get:/</a></td>
|
||||
<td>Get path /</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/request_preview?spec=get:/:b@100">get:/:b@100</a></td>
|
||||
<td>100 random bytes as the body</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href='/request_preview?spec=get:/:h"Etag"="';drop table browsers;"'>get:/:h"Etag"="';drop table browsers;"</a></td>
|
||||
<td>Add a header</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href='/request_preview?spec=get:/:u"';drop table browsers;"'>get:/:u"';drop table browsers;"</a></td>
|
||||
<td>Add a User-Agent header</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/request_preview?spec=get:/:b@100:dr">get:/:b@100:dr</a></td>
|
||||
<td>Drop the connection randomly</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/request_preview?spec="></a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/request_preview?spec=get:/:b@100,ascii:ir,@1">get:/:b@100,ascii:ir,@1</a></td>
|
||||
<td>100 ASCII bytes as the body, and randomly inject a random byte</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/request_preview?spec=ws:/">ws:/</a></td>
|
||||
<td>Initiate a websocket handshake.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
44
pathod/libpathod/templates/response_preview.html
Normal file
44
pathod/libpathod/templates/response_preview.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends "frame.html" %} {% block body %}
|
||||
<div class="page-header">
|
||||
<h1>pathod preview</h1>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px" class="row">
|
||||
<div class="span2 header">
|
||||
Specification:
|
||||
</div>
|
||||
<div class="span10">
|
||||
{% include "response_previewform.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if syntaxerror %}
|
||||
<div class="row">
|
||||
<div class="span2 header">
|
||||
Error:
|
||||
</div>
|
||||
<div class="span10">
|
||||
<p style="color: #ff0000">{{ syntaxerror }}</p>
|
||||
<pre>{{ marked }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% elif error %}
|
||||
<div class="row">
|
||||
<div class="span2 header">
|
||||
Error:
|
||||
</div>
|
||||
<div class="span10">
|
||||
<p style="color: #ff0000">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="span2 header">
|
||||
Response:
|
||||
</div>
|
||||
<div class="span10">
|
||||
<pre>{{ output }}</pre>
|
||||
<p>Note: pauses are skipped when generating previews!</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endblock %}
|
87
pathod/libpathod/templates/response_previewform.html
Normal file
87
pathod/libpathod/templates/response_previewform.html
Normal file
@ -0,0 +1,87 @@
|
||||
<form style="margin-bottom: 0" class="form-inline" method="GET" action="/response_preview">
|
||||
<input style="width: 18em" id="spec" name="spec" class="input-medium" value="{{spec}}"
|
||||
placeholder="code:[features]">
|
||||
<input type="submit" class="btn" value="preview">
|
||||
{% if not nocraft %}
|
||||
<a href="#" id="submitspec" class="btn">go</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<a class="innerlink" data-toggle="collapse" data-target="#responseexamples">examples</a>
|
||||
|
||||
<div id="responseexamples" class="collapse">
|
||||
<p>
|
||||
Check out the <a href="/docs/language">complete language docs</a>. Here are
|
||||
some examples to get you started:
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="/response_preview?spec=200">200</a></td>
|
||||
<td>A basic HTTP 200 response.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/response_preview?spec=200:r">200:r</a></td>
|
||||
<td>A basic HTTP 200 response with no Content-Length header. This will
|
||||
hang.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/response_preview?spec=200:da">200:da</a></td>
|
||||
<td>Server-side disconnect after all content has been sent.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/response_preview?spec=200:b@100">200:b@100</a></td>
|
||||
<td>
|
||||
100 random bytes as the body. A Content-Lenght header is added, so the disconnect
|
||||
is no longer needed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href='/response_preview?spec=200:b@100:h"Server"="';drop table servers;"'>200:b@100:h"Etag"="';drop table servers;"</a></td>
|
||||
<td>Add a Server header</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/response_preview?spec=200:b@100:dr">200:b@100:dr</a></td>
|
||||
<td>Drop the connection randomly</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/response_preview?spec=200:b@100,ascii:ir,@1">200:b@100,ascii:ir,@1</a></td>
|
||||
<td>100 ASCII bytes as the body, and randomly inject a random byte</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href='/response_preview?spec=200:b@1k:c"text/json"'>200:b@1k:c"text/json"</a></td>
|
||||
<td>1k of random bytes, with a text/json content type</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href='/response_preview?spec=200:b@1k:p50,120'>200:b@1k:p50,120</a></td>
|
||||
<td>1k of random bytes, pause for 120 seconds after 50 bytes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href='/response_preview?spec=200:b@1k:pr,f'>200:b@1k:pr,f</a></td>
|
||||
<td>1k of random bytes, but hang forever at a random location</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/response_preview?spec=200:b@100:h@1k,ascii_letters='foo'">200:b@100:h@1k,ascii_letters='foo'</a>
|
||||
</td>
|
||||
<td>
|
||||
100 ASCII bytes as the body, randomly generated 100k header name, with the value
|
||||
'foo'.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if not nocraft %}
|
||||
<script>
|
||||
$(function() {
|
||||
$("#submitspec").click(function() {
|
||||
document.location = "{{craftanchor}}" + $("#spec").val()
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endif %}
|
103
pathod/libpathod/test.py
Normal file
103
pathod/libpathod/test.py
Normal file
@ -0,0 +1,103 @@
|
||||
import cStringIO
|
||||
import threading
|
||||
import Queue
|
||||
|
||||
import requests
|
||||
import requests.packages.urllib3
|
||||
from . import pathod
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
|
||||
class Daemon:
|
||||
IFACE = "127.0.0.1"
|
||||
|
||||
def __init__(self, ssl=None, **daemonargs):
|
||||
self.q = Queue.Queue()
|
||||
self.logfp = cStringIO.StringIO()
|
||||
daemonargs["logfp"] = self.logfp
|
||||
self.thread = _PaThread(self.IFACE, self.q, ssl, daemonargs)
|
||||
self.thread.start()
|
||||
self.port = self.q.get(True, 5)
|
||||
self.urlbase = "%s://%s:%s" % (
|
||||
"https" if ssl else "http",
|
||||
self.IFACE,
|
||||
self.port
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.logfp.truncate(0)
|
||||
self.shutdown()
|
||||
return False
|
||||
|
||||
def p(self, spec):
|
||||
"""
|
||||
Return a URL that will render the response in spec.
|
||||
"""
|
||||
return "%s/p/%s" % (self.urlbase, spec)
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Return some basic info about the remote daemon.
|
||||
"""
|
||||
resp = requests.get("%s/api/info" % self.urlbase, verify=False)
|
||||
return resp.json()
|
||||
|
||||
def text_log(self):
|
||||
return self.logfp.getvalue()
|
||||
|
||||
def last_log(self):
|
||||
"""
|
||||
Returns the last logged request, or None.
|
||||
"""
|
||||
l = self.log()
|
||||
if not l:
|
||||
return None
|
||||
return l[0]
|
||||
|
||||
def log(self):
|
||||
"""
|
||||
Return the log buffer as a list of dictionaries.
|
||||
"""
|
||||
resp = requests.get("%s/api/log" % self.urlbase, verify=False)
|
||||
return resp.json()["log"]
|
||||
|
||||
def clear_log(self):
|
||||
"""
|
||||
Clear the log.
|
||||
"""
|
||||
self.logfp.truncate(0)
|
||||
resp = requests.get("%s/api/clear_log" % self.urlbase, verify=False)
|
||||
return resp.ok
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shut the daemon down, return after the thread has exited.
|
||||
"""
|
||||
self.thread.server.shutdown()
|
||||
self.thread.join()
|
||||
|
||||
|
||||
class _PaThread(threading.Thread):
|
||||
|
||||
def __init__(self, iface, q, ssl, daemonargs):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "PathodThread"
|
||||
self.iface, self.q, self.ssl = iface, q, ssl
|
||||
self.daemonargs = daemonargs
|
||||
|
||||
def run(self):
|
||||
self.server = pathod.Pathod(
|
||||
(self.iface, 0),
|
||||
ssl=self.ssl,
|
||||
**self.daemonargs
|
||||
)
|
||||
self.name = "PathodThread (%s:%s)" % (
|
||||
self.server.address.host,
|
||||
self.server.address.port
|
||||
)
|
||||
self.q.put(self.server.address.port)
|
||||
self.server.serve_forever()
|
124
pathod/libpathod/utils.py
Normal file
124
pathod/libpathod/utils.py
Normal file
@ -0,0 +1,124 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
SIZE_UNITS = dict(
|
||||
b=1024 ** 0,
|
||||
k=1024 ** 1,
|
||||
m=1024 ** 2,
|
||||
g=1024 ** 3,
|
||||
t=1024 ** 4,
|
||||
)
|
||||
|
||||
|
||||
class MemBool(object):
|
||||
|
||||
"""
|
||||
Truth-checking with a memory, for use in chained if statements.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.v = None
|
||||
|
||||
def __call__(self, v):
|
||||
self.v = v
|
||||
return bool(v)
|
||||
|
||||
|
||||
def parse_size(s):
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
pass
|
||||
for i in SIZE_UNITS.keys():
|
||||
if s.endswith(i):
|
||||
try:
|
||||
return int(s[:-1]) * SIZE_UNITS[i]
|
||||
except ValueError:
|
||||
break
|
||||
raise ValueError("Invalid size specification.")
|
||||
|
||||
|
||||
def parse_anchor_spec(s):
|
||||
"""
|
||||
Return a tuple, or None on error.
|
||||
"""
|
||||
if "=" not in s:
|
||||
return None
|
||||
return tuple(s.split("=", 1))
|
||||
|
||||
|
||||
def xrepr(s):
|
||||
return repr(s)[1:-1]
|
||||
|
||||
|
||||
def inner_repr(s):
|
||||
"""
|
||||
Returns the inner portion of a string or unicode repr (i.e. without the
|
||||
quotes)
|
||||
"""
|
||||
if isinstance(s, unicode):
|
||||
return repr(s)[2:-1]
|
||||
else:
|
||||
return repr(s)[1:-1]
|
||||
|
||||
|
||||
def escape_unprintables(s):
|
||||
"""
|
||||
Like inner_repr, but preserves line breaks.
|
||||
"""
|
||||
s = s.replace("\r\n", "PATHOD_MARKER_RN")
|
||||
s = s.replace("\n", "PATHOD_MARKER_N")
|
||||
s = inner_repr(s)
|
||||
s = s.replace("PATHOD_MARKER_RN", "\n")
|
||||
s = s.replace("PATHOD_MARKER_N", "\n")
|
||||
return s
|
||||
|
||||
|
||||
class Data(object):
|
||||
|
||||
def __init__(self, name):
|
||||
m = __import__(name)
|
||||
dirname, _ = os.path.split(m.__file__)
|
||||
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
|
||||
|
||||
|
||||
data = Data(__name__)
|
||||
|
||||
|
||||
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): # pragma: nocover
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as e:
|
||||
sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||
sys.exit(1)
|
||||
os.chdir("/")
|
||||
os.umask(0)
|
||||
os.setsid()
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as e:
|
||||
sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||
sys.exit(1)
|
||||
si = open(stdin, 'rb')
|
||||
so = open(stdout, 'a+b')
|
||||
se = open(stderr, 'a+b', 0)
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
11
pathod/libpathod/version.py
Normal file
11
pathod/libpathod/version.py
Normal file
@ -0,0 +1,11 @@
|
||||
from __future__ import (absolute_import, print_function, division)
|
||||
|
||||
IVERSION = (0, 17)
|
||||
VERSION = ".".join(str(i) for i in IVERSION)
|
||||
MINORVERSION = ".".join(str(i) for i in IVERSION[:2])
|
||||
NAME = "pathod"
|
||||
NAMEVERSION = NAME + " " + VERSION
|
||||
|
||||
NEXT_MINORVERSION = list(IVERSION)
|
||||
NEXT_MINORVERSION[1] += 1
|
||||
NEXT_MINORVERSION = ".".join(str(i) for i in NEXT_MINORVERSION[:2])
|
6
pathod/pathoc
Executable file
6
pathod/pathoc
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from libpathod import pathoc_cmdline as cmdline
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmdline.go_pathoc()
|
6
pathod/pathod
Executable file
6
pathod/pathod
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from libpathod import pathod_cmdline as cmdline
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmdline.go_pathod()
|
22
pathod/release/pathoc.spec
Normal file
22
pathod/release/pathoc.spec
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
|
||||
a = Analysis(['../pathoc'],
|
||||
binaries=None,
|
||||
datas=None,
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=None,
|
||||
runtime_hooks=None,
|
||||
excludes=None)
|
||||
pyz = PYZ(a.pure, a.zipped_data)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='pathoc',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=True )
|
22
pathod/release/pathod.spec
Normal file
22
pathod/release/pathod.spec
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
|
||||
a = Analysis(['../pathod'],
|
||||
binaries=None,
|
||||
datas=collect_data_files("libpathod"),
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=None,
|
||||
runtime_hooks=None,
|
||||
excludes=None)
|
||||
pyz = PYZ(a.pure, a.zipped_data)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='pathod',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=True )
|
2
pathod/requirements.txt
Normal file
2
pathod/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
-e git+https://github.com/mitmproxy/netlib.git#egg=netlib
|
||||
-e .[dev]
|
63
pathod/setup.py
Normal file
63
pathod/setup.py
Normal file
@ -0,0 +1,63 @@
|
||||
from setuptools import setup, find_packages
|
||||
from codecs import open
|
||||
import os
|
||||
from libpathod import version
|
||||
|
||||
# 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__))
|
||||
|
||||
with open(os.path.join(here, 'README.txt'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name="pathod",
|
||||
version=version.VERSION,
|
||||
description="A pathological HTTP/S daemon for testing and stressing clients.",
|
||||
long_description=long_description,
|
||||
url="http://pathod.net",
|
||||
author="Aldo Cortesi",
|
||||
author_email="aldo@corte.si",
|
||||
license="MIT",
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"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 :: Internet",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"Topic :: Software Development :: Testing :: Traffic Generation",
|
||||
],
|
||||
|
||||
packages=find_packages(exclude=["test", "test.*"]),
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
"pathod = libpathod.pathod_cmdline:go_pathod",
|
||||
"pathoc = libpathod.pathoc_cmdline:go_pathoc"
|
||||
]
|
||||
},
|
||||
install_requires=[
|
||||
"netlib>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION),
|
||||
"requests>=2.9.1, <2.10",
|
||||
"Flask>=0.10.1, <0.11",
|
||||
"pyparsing>=2.1,<2.2"
|
||||
],
|
||||
extras_require={
|
||||
'dev': [
|
||||
"mock>=1.3.0, <1.4",
|
||||
"pytest>=2.8.0",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
)
|
3
pathod/test/data/clientcert/.gitignore
vendored
Normal file
3
pathod/test/data/clientcert/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
client.crt
|
||||
client.key
|
||||
client.req
|
5
pathod/test/data/clientcert/client.cnf
Normal file
5
pathod/test/data/clientcert/client.cnf
Normal file
@ -0,0 +1,5 @@
|
||||
[ ssl_client ]
|
||||
basicConstraints = CA:FALSE
|
||||
nsCertType = client
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth
|
42
pathod/test/data/clientcert/client.pem
Normal file
42
pathod/test/data/clientcert/client.pem
Normal file
@ -0,0 +1,42 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0
|
||||
EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+
|
||||
ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G
|
||||
3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/
|
||||
SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP
|
||||
G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABAoIBAFE3FV/IDltbmHEP
|
||||
iky93hbJm+6QgKepFReKpRVTyqb7LaygUvueQyPWQMIriKTsy675nxo8DQr7tQsO
|
||||
y3YlSZgra/xNMikIB6e82c7K8DgyrDQw/rCqjZB3Xt4VCqsWJDLXnQMSn98lx0g7
|
||||
d7Lbf8soUpKWXqfdVpSDTi4fibSX6kshXyfSTpcz4AdoncEpViUfU1xkEEmZrjT8
|
||||
1GcCsDC41xdNmzCpqRuZX7DKSFRoB+0hUzsC1oiqM7FD5kixonRd4F5PbRXImIzt
|
||||
6YCsT2okxTA04jX7yByis7LlOLTlkmLtKQYuc3erOFvwx89s4vW+AeFei+GGNitn
|
||||
tHfSwbECgYEA7SzV+nN62hAERHlg8cEQT4TxnsWvbronYWcc/ev44eHSPDWL5tPi
|
||||
GHfSbW6YAq5Wa0I9jMWfXyhOYEC3MZTC5EEeLOB71qVrTwcy/sY66rOrcgjFI76Q
|
||||
5JFHQ4wy3SWU50KxE0oWJO9LIowprG+pW1vzqC3VF0T7q0FqESrY4LUCgYEA3F7Z
|
||||
80ndnCUlooJAb+Hfotv7peFf1o6+m1PTRcz1lLnVt5R5lXj86kn+tXEpYZo1RiGR
|
||||
2rE2N0seeznWCooakHcsBN7/qmFIhhooJNF7yW+JP2I4P2UV5+tJ+8bcs/voUkQD
|
||||
1x+rGOuMn8nvHBd2+Vharft8eGL2mgooPVI2XusCgYEAlMZpO3+w8pTVeHaDP2MR
|
||||
7i/AuQ3cbCLNjSX3Y7jgGCFllWspZRRIYXzYPNkA9b2SbBnTLjjRLgnEkFBIGgvs
|
||||
7O2EFjaCuDRvydUEQhjq4ErwIsopj7B8h0QyZcbOKTbn3uFQ3n68wVJx2Sv/ADHT
|
||||
FIHrp/WIE96r19Niy34LKXkCgYB2W59VsuOKnMz01l5DeR5C+0HSWxS9SReIl2IO
|
||||
yEFSKullWyJeLIgyUaGy0990430feKI8whcrZXYumuah7IDN/KOwzhCk8vEfzWao
|
||||
N7bzfqtJVrh9HA7C7DVlO+6H4JFrtcoWPZUIomJ549w/yz6EN3ckoMC+a/Ck1TW9
|
||||
ka1QFwKBgQCywG6TrZz0UmOjyLQZ+8Q4uvZklSW5NAKBkNnyuQ2kd5rzyYgMPE8C
|
||||
Er8T88fdVIKvkhDyHhwcI7n58xE5Gr7wkwsrk/Hbd9/ZB2GgAPY3cATskK1v1McU
|
||||
YeX38CU0fUS4aoy26hWQXkViB47IGQ3jWo3ZCtzIJl8DI9/RsBWTnw==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICYDCCAckCAQEwDQYJKoZIhvcNAQEFBQAwKDESMBAGA1UEAxMJbWl0bXByb3h5
|
||||
MRIwEAYDVQQKEwltaXRtcHJveHkwHhcNMTMwMTIwMDEwODEzWhcNMTUxMDE3MDEw
|
||||
ODEzWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UE
|
||||
ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0
|
||||
EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+
|
||||
ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G
|
||||
3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/
|
||||
SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP
|
||||
G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABMA0GCSqGSIb3DQEBBQUA
|
||||
A4GBAFvI+cd47B85PQ970n2dU/PlA2/Hb1ldrrXh2guR4hX6vYx/uuk5yRI/n0Rd
|
||||
KOXJ3czO0bd2Fpe3ZoNpkW0pOSDej/Q+58ScuJd0gWCT/Sh1eRk6ZdC0kusOuWoY
|
||||
bPOPMkG45LPgUMFOnZEsfJP6P5mZIxlbCvSMFC25nPHWlct7
|
||||
-----END CERTIFICATE-----
|
8
pathod/test/data/clientcert/make
Executable file
8
pathod/test/data/clientcert/make
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
openssl genrsa -out client.key 2048
|
||||
openssl req -key client.key -new -out client.req
|
||||
openssl x509 -req -days 365 -in client.req -signkey client.key -out client.crt -extfile client.cnf -extensions ssl_client
|
||||
openssl x509 -req -days 1000 -in client.req -CA ~/.mitmproxy/mitmproxy-ca.pem -CAkey ~/.mitmproxy/mitmproxy-ca.pem -set_serial 00001 -out client.crt -extensions ssl_client
|
||||
cat client.key client.crt > client.pem
|
||||
openssl x509 -text -noout -in client.pem
|
1
pathod/test/data/file
Normal file
1
pathod/test/data/file
Normal file
@ -0,0 +1 @@
|
||||
testfile
|
1
pathod/test/data/request
Normal file
1
pathod/test/data/request
Normal file
@ -0,0 +1 @@
|
||||
get:/foo
|
1
pathod/test/data/response
Normal file
1
pathod/test/data/response
Normal file
@ -0,0 +1 @@
|
||||
202
|
68
pathod/test/data/testkey.pem
Normal file
68
pathod/test/data/testkey.pem
Normal file
@ -0,0 +1,68 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIG5QIBAAKCAYEAwvtKxoZvBV2AxPAkCx8PXbuE7KeqK9bBvk8x+JchPMdf/KZj
|
||||
sdu2v6Gm8Hi053i7ZGxouFvonJxHAiK6cwk9OYQwa9fbOFf2mgWKEBO4fbCH93tW
|
||||
DCTdWVxFyNViAvxGHlJs3/IU03pIG29AgUnhRW8pGbabAfx8emcOZJZ3ykEuimaC
|
||||
4s7mRwdc63GXnbcjTtRkrJsBATI+xvPwuR2+4daX7sPCf0kel3bN2jMpwXfvk/Ww
|
||||
kJ2BIEeZCg0qIvyMjH9qrUirUnsmQnpPln0CGBbQEBsW9yMfGoFdREiMYls5jZeq
|
||||
NxjWNv1RTRIm/4RjMwyxnoTA9eDS9wwO2NnJS4vfXAnUTP4BYx8Pe4ZMA2Gm6YrC
|
||||
ysT6YA1xdHNpcuHXClxwmPj/cm8Z5kIg5clbNIK60ts9yFr/Ao3KPPYJ2GBv8/Oe
|
||||
ApPBJuubews+/9/13Ew/SJ1t2u28+sPbgXUG8dC2n4vWTvJwKf6Duqxgnm82zdzj
|
||||
SZoXRQsP984qiN7NAgMBAAECggGBALB6rqWdzCL5DLI0AQun40qdjaR95UKksNvF
|
||||
5p7we379nl2ZZKb5DSHJ+MWzG1pfJo2wqeAkIBiQQp0mPcgdVrMWeJVD3QHUbDng
|
||||
RaRjlRr+izJvCeUYANj+8ZLjwECfgf+z7yOLg1oeVeGvAp2C90jXYkYJx6c2lpxb
|
||||
ZuWYY3hHIw7V1iXfywIDIhFg0TBJMMYK68xmx7QDfFqrNPj4eWsDxqSvvv1iezPw
|
||||
rkWPBX49RjWPrW5XgSZsZ5J3c+oS1rZmIY7EAgopTWB/3wJjZR1Idz/9l9LIWlBP
|
||||
6zVC27CIZzSEeGguqNVeyzJ0TPWh5idYNRmSZr6eTUF0245LNO/gqvWKgRSNIZko
|
||||
HoBa2F1AvCiB67S1kxjwS5y3VkudZE4jkgGKcC2Ws/9QmOZ0HAsjI8VAMp2hj6iN
|
||||
0HdPMTNtsLgbhKyXsoZuW4YmwfSTPxGi2gvcI7GUozpTz84n1cOneJnz1ygx6Uru
|
||||
v8DpQg+VX6xTy4X6AK1F8OYNMZ/jaQKBwQDv30NevQStnGbTmcSr+0fd4rmWFklK
|
||||
V6B2X7zWynVpSGvCWkqVSp3mG6aiZItAltVMRL/9LT6zIheyClyd+vXIjR2+W210
|
||||
XMxrvz7A8qCXkvB2dyEhrMdCfZ7p8+kf+eD2c/Mnxb7VpmDfHYLx30JeQoBwjrwU
|
||||
Eul+dE1P+r8bWBaLTjlsipTya74yItWWAToXAo+s1BXBtXhEsLoe4FghlC0u724d
|
||||
ucjDaeICdLcerApdvg6Q6p4kVHaoF6ka6I8CgcEA0Bdc05ery9gLC6CclV+BhA5Q
|
||||
dfDq2P7qhc7e1ipwNRrQo2gy5HhgOkTL3dJWc+8rV6CBP/JfchnsW40tDOnPCTLT
|
||||
gg3n7vv3RHrtncApXuhIFR+B5xjohTPBzxRUMiAOre2d0F5b6eBXFjptf/1i2tQ+
|
||||
qdqJoyOGOZP0hKVslGIfz+CKc6WEkIqX7c91Msdr5myeaWDI5TsurfuKRBH395T3
|
||||
BMAi6oinAAEb1rdySenLO2A/0kVmBVlTpaN3TNjjAoHBAMvS4uQ1qSv8okNbfgrF
|
||||
UqPwa9JkzZImM2tinovFLU9xAl/7aTTCWrmU9Vs4JDuV71kHcjwnngeJCKl4tIpp
|
||||
HUB06Lk/5xnhYLKNpz087cjeSwXe5IBA2HBfXhFd+NH6+nVwwUUieq4A2n+8C/CK
|
||||
zVJbH9iE8Lv99fpFyQwU/R63EzD8Hz9j4ny7oLnpb6QvFrVGr98jt/kJwlBb+0sR
|
||||
RtIBnwMq4F7R5w5lgm6jzpZ5ibVuMeJh+k7Ulp7uu/rpcQKBwQDE3sWIvf7f7PaO
|
||||
OpbJz0CmYjCHVLWrNIlGrPAv6Jid9U+cuXEkrCpGFl5V77CxIH59+bEuga0BMztl
|
||||
ZkxP4khoqHhom6VpeWJ3nGGAFJRPYS0JJvTsYalilBPxSYdaoO+iZ6MdxpfozcE2
|
||||
m3KLW3uSEqlyYvpCqNJNWQhGEoeGXstADWyPevHPGgAhElwL/ZW8u9inU9Tc4sAI
|
||||
BGnMer+BsaJ+ERU3lK+Clony+z2aZiFLfIUE93lM6DT2CZBN2QcCgcAVk4L0bfA6
|
||||
HFnP/ZWNlnYWpOVFKcq57PX+J5/k7Tf34e2cYM2P0eqYggWZbzVd8qoCOQCHrAx0
|
||||
aZSSvEyKAVvzRNeqbm1oXaMojksMnrSX5henHjPbZlr1EmM7+zMnSTMkfVOx/6g1
|
||||
97sASej31XdOAgKCBJGymrwvYrCLW+P5cHqd+D8v/PvfpRIQM54p5ixRt3EYZvtR
|
||||
zGrzsr0OGyOLZtj1DB0a3kvajAAOCl3TawJSzviKo2mwc+/xj28MCQM=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE4TCCA0mgAwIBAgIJALONCAWZxPhUMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV
|
||||
BAYTAk5aMQ4wDAYDVQQIDAVPdGFnbzEPMA0GA1UECgwGUGF0aG9kMREwDwYDVQQD
|
||||
DAh0ZXN0LmNvbTAeFw0xNTA0MTgyMjA0NTNaFw00MjA5MDIyMjA0NTNaMEExCzAJ
|
||||
BgNVBAYTAk5aMQ4wDAYDVQQIDAVPdGFnbzEPMA0GA1UECgwGUGF0aG9kMREwDwYD
|
||||
VQQDDAh0ZXN0LmNvbTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAML7
|
||||
SsaGbwVdgMTwJAsfD127hOynqivWwb5PMfiXITzHX/ymY7Hbtr+hpvB4tOd4u2Rs
|
||||
aLhb6JycRwIiunMJPTmEMGvX2zhX9poFihATuH2wh/d7Vgwk3VlcRcjVYgL8Rh5S
|
||||
bN/yFNN6SBtvQIFJ4UVvKRm2mwH8fHpnDmSWd8pBLopmguLO5kcHXOtxl523I07U
|
||||
ZKybAQEyPsbz8LkdvuHWl+7Dwn9JHpd2zdozKcF375P1sJCdgSBHmQoNKiL8jIx/
|
||||
aq1Iq1J7JkJ6T5Z9AhgW0BAbFvcjHxqBXURIjGJbOY2XqjcY1jb9UU0SJv+EYzMM
|
||||
sZ6EwPXg0vcMDtjZyUuL31wJ1Ez+AWMfD3uGTANhpumKwsrE+mANcXRzaXLh1wpc
|
||||
cJj4/3JvGeZCIOXJWzSCutLbPcha/wKNyjz2Cdhgb/PzngKTwSbrm3sLPv/f9dxM
|
||||
P0idbdrtvPrD24F1BvHQtp+L1k7ycCn+g7qsYJ5vNs3c40maF0ULD/fOKojezQID
|
||||
AQABo4HbMIHYMAsGA1UdDwQEAwIFoDAdBgNVHQ4EFgQUbEgfTauEqEP/bnBtby1K
|
||||
bihJvcswcQYDVR0jBGowaIAUbEgfTauEqEP/bnBtby1KbihJvcuhRaRDMEExCzAJ
|
||||
BgNVBAYTAk5aMQ4wDAYDVQQIDAVPdGFnbzEPMA0GA1UECgwGUGF0aG9kMREwDwYD
|
||||
VQQDDAh0ZXN0LmNvbYIJALONCAWZxPhUMAwGA1UdEwQFMAMBAf8wKQYDVR0RBCIw
|
||||
IIIIdGVzdC5jb22CCXRlc3QyLmNvbYIJdGVzdDMuY29tMA0GCSqGSIb3DQEBCwUA
|
||||
A4IBgQBcTedXtUb91DxQRtg73iomz7cQ4niZntUBW8iE5rpoA7prtQNGHMCbHwaX
|
||||
tbWFkzBmL5JTBWvd/6AQ2LtiB3rYB3W/iRhbpsNJ501xaoOguPEQ9720Ph8TEveM
|
||||
208gNzGsEOcNALwyXj2y9M19NGu9zMa8eu1Tc3IsQaVaGKHx8XZn5HTNUx8EdcwI
|
||||
Z/Ji9ETDCL7+e5INv0tqfFSazWaQUwxM4IzPMkKTYRcMuN/6eog609k9r9pp32Ut
|
||||
rKlzc6GIkAlgJJ0Wkoz1V46DmJNJdJG7eLu/mtsB85j6hytIQeWTf1fll5YnMZLF
|
||||
HgNZtfYn8Q0oTdBQ0ZOaZeQCfZ8emYBdLJf2YB83uGRMjQ1FoeIxzQqiRq8WHRdb
|
||||
9Q45i0DINMnNp0DbLMA4numZ7wT9SQb6sql9eUyuCNDw7nGIWTHUNfLtU1Er3h1d
|
||||
icJuApx9+//UN/pGh0yTXb3fZbiI4IehRmkpnIWonIAwaVGm6JZU04wiIn8CuBho
|
||||
/qQdlS8=
|
||||
-----END CERTIFICATE-----
|
17
pathod/test/scripts/generate.sh
Executable file
17
pathod/test/scripts/generate.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f ./private.key ]
|
||||
then
|
||||
openssl genrsa -out private.key 3072
|
||||
fi
|
||||
openssl req \
|
||||
-batch \
|
||||
-new -x509 \
|
||||
-key private.key \
|
||||
-sha256 \
|
||||
-out cert.pem \
|
||||
-days 9999 \
|
||||
-config ./openssl.cnf
|
||||
openssl x509 -in cert.pem -text -noout
|
||||
cat ./private.key ./cert.pem > testcert.pem
|
||||
rm ./private.key ./cert.pem
|
39
pathod/test/scripts/openssl.cnf
Normal file
39
pathod/test/scripts/openssl.cnf
Normal file
@ -0,0 +1,39 @@
|
||||
[ req ]
|
||||
default_bits = 1024
|
||||
default_keyfile = privkey.pem
|
||||
distinguished_name = req_distinguished_name
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[ req_distinguished_name ]
|
||||
countryName = Country Name (2 letter code)
|
||||
countryName_default = NZ
|
||||
countryName_min = 2
|
||||
countryName_max = 2
|
||||
stateOrProvinceName = State or Province Name (full name)
|
||||
stateOrProvinceName_default = Otago
|
||||
localityName = Locality Name (eg, city)
|
||||
0.organizationName = Organization Name (eg, company)
|
||||
0.organizationName_default = Pathod
|
||||
commonName = Common Name (e.g. server FQDN or YOUR name)
|
||||
commonName_default = test.com
|
||||
commonName_max = 64
|
||||
|
||||
[ v3_req ]
|
||||
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
|
||||
[ v3_ca ]
|
||||
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid:always,issuer:always
|
||||
basicConstraints = CA:true
|
||||
subjectAltName = @alternate_names
|
||||
|
||||
|
||||
[ alternate_names ]
|
||||
|
||||
DNS.1 = test.com
|
||||
DNS.2 = test2.com
|
||||
DNS.3 = test3.com
|
85
pathod/test/test_app.py
Normal file
85
pathod/test/test_app.py
Normal file
@ -0,0 +1,85 @@
|
||||
import tutils
|
||||
|
||||
|
||||
class TestApp(tutils.DaemonTests):
|
||||
SSL = False
|
||||
|
||||
def test_index(self):
|
||||
r = self.getpath("/")
|
||||
assert r.status_code == 200
|
||||
assert r.content
|
||||
|
||||
def test_about(self):
|
||||
r = self.getpath("/about")
|
||||
assert r.ok
|
||||
|
||||
def test_download(self):
|
||||
r = self.getpath("/download")
|
||||
assert r.ok
|
||||
|
||||
def test_docs(self):
|
||||
assert self.getpath("/docs/pathod").status_code == 200
|
||||
assert self.getpath("/docs/pathoc").status_code == 200
|
||||
assert self.getpath("/docs/language").status_code == 200
|
||||
assert self.getpath("/docs/libpathod").status_code == 200
|
||||
assert self.getpath("/docs/test").status_code == 200
|
||||
|
||||
def test_log(self):
|
||||
assert self.getpath("/log").status_code == 200
|
||||
assert self.get("200:da").status_code == 200
|
||||
id = self.d.log()[0]["id"]
|
||||
assert self.getpath("/log").status_code == 200
|
||||
assert self.getpath("/log/%s" % id).status_code == 200
|
||||
assert self.getpath("/log/9999999").status_code == 404
|
||||
|
||||
def test_log_binary(self):
|
||||
assert self.get("200:h@10b=@10b:da")
|
||||
|
||||
def test_response_preview(self):
|
||||
r = self.getpath("/response_preview", params=dict(spec="200"))
|
||||
assert r.status_code == 200
|
||||
assert 'Response' in r.content
|
||||
|
||||
r = self.getpath("/response_preview", params=dict(spec="foo"))
|
||||
assert r.status_code == 200
|
||||
assert 'Error' in r.content
|
||||
|
||||
r = self.getpath("/response_preview", params=dict(spec="200:b@100m"))
|
||||
assert r.status_code == 200
|
||||
assert "too large" in r.content
|
||||
|
||||
r = self.getpath("/response_preview", params=dict(spec="200:b@5k"))
|
||||
assert r.status_code == 200
|
||||
assert 'Response' in r.content
|
||||
|
||||
r = self.getpath(
|
||||
"/response_preview",
|
||||
params=dict(
|
||||
spec="200:b<nonexistent"))
|
||||
assert r.status_code == 200
|
||||
assert 'File access denied' in r.content
|
||||
|
||||
r = self.getpath("/response_preview", params=dict(spec="200:b<file"))
|
||||
assert r.status_code == 200
|
||||
assert 'testfile' in r.content
|
||||
|
||||
def test_request_preview(self):
|
||||
r = self.getpath("/request_preview", params=dict(spec="get:/"))
|
||||
assert r.status_code == 200
|
||||
assert 'Request' in r.content
|
||||
|
||||
r = self.getpath("/request_preview", params=dict(spec="foo"))
|
||||
assert r.status_code == 200
|
||||
assert 'Error' in r.content
|
||||
|
||||
r = self.getpath("/request_preview", params=dict(spec="get:/:b@100m"))
|
||||
assert r.status_code == 200
|
||||
assert "too large" in r.content
|
||||
|
||||
r = self.getpath("/request_preview", params=dict(spec="get:/:b@5k"))
|
||||
assert r.status_code == 200
|
||||
assert 'Request' in r.content
|
||||
|
||||
r = self.getpath("/request_preview", params=dict(spec=""))
|
||||
assert r.status_code == 200
|
||||
assert 'empty spec' in r.content
|
135
pathod/test/test_language_actions.py
Normal file
135
pathod/test/test_language_actions.py
Normal file
@ -0,0 +1,135 @@
|
||||
import cStringIO
|
||||
|
||||
from libpathod.language import actions
|
||||
from libpathod import language
|
||||
|
||||
|
||||
def parse_request(s):
|
||||
return language.parse_pathoc(s).next()
|
||||
|
||||
|
||||
def test_unique_name():
|
||||
assert not actions.PauseAt(0, "f").unique_name
|
||||
assert actions.DisconnectAt(0).unique_name
|
||||
|
||||
|
||||
class TestDisconnects:
|
||||
|
||||
def test_parse_pathod(self):
|
||||
a = language.parse_pathod("400:d0").next().actions[0]
|
||||
assert a.spec() == "d0"
|
||||
a = language.parse_pathod("400:dr").next().actions[0]
|
||||
assert a.spec() == "dr"
|
||||
|
||||
def test_at(self):
|
||||
e = actions.DisconnectAt.expr()
|
||||
v = e.parseString("d0")[0]
|
||||
assert isinstance(v, actions.DisconnectAt)
|
||||
assert v.offset == 0
|
||||
|
||||
v = e.parseString("d100")[0]
|
||||
assert v.offset == 100
|
||||
|
||||
e = actions.DisconnectAt.expr()
|
||||
v = e.parseString("dr")[0]
|
||||
assert v.offset == "r"
|
||||
|
||||
def test_spec(self):
|
||||
assert actions.DisconnectAt("r").spec() == "dr"
|
||||
assert actions.DisconnectAt(10).spec() == "d10"
|
||||
|
||||
|
||||
class TestInject:
|
||||
|
||||
def test_parse_pathod(self):
|
||||
a = language.parse_pathod("400:ir,@100").next().actions[0]
|
||||
assert a.offset == "r"
|
||||
assert a.value.datatype == "bytes"
|
||||
assert a.value.usize == 100
|
||||
|
||||
a = language.parse_pathod("400:ia,@100").next().actions[0]
|
||||
assert a.offset == "a"
|
||||
|
||||
def test_at(self):
|
||||
e = actions.InjectAt.expr()
|
||||
v = e.parseString("i0,'foo'")[0]
|
||||
assert v.value.val == "foo"
|
||||
assert v.offset == 0
|
||||
assert isinstance(v, actions.InjectAt)
|
||||
|
||||
v = e.parseString("ir,'foo'")[0]
|
||||
assert v.offset == "r"
|
||||
|
||||
def test_serve(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = language.parse_pathod("400:i0,'foo'").next()
|
||||
assert language.serve(r, s, {})
|
||||
|
||||
def test_spec(self):
|
||||
e = actions.InjectAt.expr()
|
||||
v = e.parseString("i0,'foo'")[0]
|
||||
assert v.spec() == 'i0,"foo"'
|
||||
|
||||
def test_spec(self):
|
||||
e = actions.InjectAt.expr()
|
||||
v = e.parseString("i0,@100")[0]
|
||||
v2 = v.freeze({})
|
||||
v3 = v2.freeze({})
|
||||
assert v2.value.val == v3.value.val
|
||||
|
||||
|
||||
class TestPauses:
|
||||
|
||||
def test_parse_pathod(self):
|
||||
e = actions.PauseAt.expr()
|
||||
v = e.parseString("p10,10")[0]
|
||||
assert v.seconds == 10
|
||||
assert v.offset == 10
|
||||
|
||||
v = e.parseString("p10,f")[0]
|
||||
assert v.seconds == "f"
|
||||
|
||||
v = e.parseString("pr,f")[0]
|
||||
assert v.offset == "r"
|
||||
|
||||
v = e.parseString("pa,f")[0]
|
||||
assert v.offset == "a"
|
||||
|
||||
def test_request(self):
|
||||
r = language.parse_pathod('400:p10,10').next()
|
||||
assert r.actions[0].spec() == "p10,10"
|
||||
|
||||
def test_spec(self):
|
||||
assert actions.PauseAt("r", 5).spec() == "pr,5"
|
||||
assert actions.PauseAt(0, 5).spec() == "p0,5"
|
||||
assert actions.PauseAt(0, "f").spec() == "p0,f"
|
||||
|
||||
def test_freeze(self):
|
||||
l = actions.PauseAt("r", 5)
|
||||
assert l.freeze({}).spec() == l.spec()
|
||||
|
||||
|
||||
class Test_Action:
|
||||
|
||||
def test_cmp(self):
|
||||
a = actions.DisconnectAt(0)
|
||||
b = actions.DisconnectAt(1)
|
||||
c = actions.DisconnectAt(0)
|
||||
assert a < b
|
||||
assert a == c
|
||||
l = sorted([b, a])
|
||||
assert l[0].offset == 0
|
||||
|
||||
def test_resolve(self):
|
||||
r = parse_request('GET:"/foo"')
|
||||
e = actions.DisconnectAt("r")
|
||||
ret = e.resolve({}, r)
|
||||
assert isinstance(ret.offset, int)
|
||||
|
||||
def test_repr(self):
|
||||
e = actions.DisconnectAt("r")
|
||||
assert repr(e)
|
||||
|
||||
def test_freeze(self):
|
||||
l = actions.DisconnectAt(5)
|
||||
assert l.freeze({}).spec() == l.spec()
|
352
pathod/test/test_language_base.py
Normal file
352
pathod/test/test_language_base.py
Normal file
@ -0,0 +1,352 @@
|
||||
import os
|
||||
from libpathod import language
|
||||
from libpathod.language import base, exceptions
|
||||
import tutils
|
||||
|
||||
|
||||
def parse_request(s):
|
||||
return language.parse_pathoc(s).next()
|
||||
|
||||
|
||||
def test_times():
|
||||
reqs = list(language.parse_pathoc("get:/:x5"))
|
||||
assert len(reqs) == 5
|
||||
assert not reqs[0].times
|
||||
|
||||
|
||||
def test_caseless_literal():
|
||||
class CL(base.CaselessLiteral):
|
||||
TOK = "foo"
|
||||
v = CL("foo")
|
||||
assert v.expr()
|
||||
assert v.values(language.Settings())
|
||||
|
||||
|
||||
class TestTokValueNakedLiteral:
|
||||
|
||||
def test_expr(self):
|
||||
v = base.TokValueNakedLiteral("foo")
|
||||
assert v.expr()
|
||||
|
||||
def test_spec(self):
|
||||
v = base.TokValueNakedLiteral("foo")
|
||||
assert v.spec() == repr(v) == "foo"
|
||||
|
||||
v = base.TokValueNakedLiteral("f\x00oo")
|
||||
assert v.spec() == repr(v) == r"f\x00oo"
|
||||
|
||||
|
||||
class TestTokValueLiteral:
|
||||
|
||||
def test_espr(self):
|
||||
v = base.TokValueLiteral("foo")
|
||||
assert v.expr()
|
||||
assert v.val == "foo"
|
||||
|
||||
v = base.TokValueLiteral("foo\n")
|
||||
assert v.expr()
|
||||
assert v.val == "foo\n"
|
||||
assert repr(v)
|
||||
|
||||
def test_spec(self):
|
||||
v = base.TokValueLiteral("foo")
|
||||
assert v.spec() == r"'foo'"
|
||||
|
||||
v = base.TokValueLiteral("f\x00oo")
|
||||
assert v.spec() == repr(v) == r"'f\x00oo'"
|
||||
|
||||
v = base.TokValueLiteral("\"")
|
||||
assert v.spec() == repr(v) == '\'"\''
|
||||
|
||||
def roundtrip(self, spec):
|
||||
e = base.TokValueLiteral.expr()
|
||||
v = base.TokValueLiteral(spec)
|
||||
v2 = e.parseString(v.spec())
|
||||
assert v.val == v2[0].val
|
||||
assert v.spec() == v2[0].spec()
|
||||
|
||||
def test_roundtrip(self):
|
||||
self.roundtrip("'")
|
||||
self.roundtrip('\'')
|
||||
self.roundtrip("a")
|
||||
self.roundtrip("\"")
|
||||
# self.roundtrip("\\")
|
||||
self.roundtrip("200:b'foo':i23,'\\''")
|
||||
self.roundtrip("\a")
|
||||
|
||||
|
||||
class TestTokValueGenerate:
|
||||
|
||||
def test_basic(self):
|
||||
v = base.TokValue.parseString("@10b")[0]
|
||||
assert v.usize == 10
|
||||
assert v.unit == "b"
|
||||
assert v.bytes() == 10
|
||||
v = base.TokValue.parseString("@10")[0]
|
||||
assert v.unit == "b"
|
||||
v = base.TokValue.parseString("@10k")[0]
|
||||
assert v.bytes() == 10240
|
||||
v = base.TokValue.parseString("@10g")[0]
|
||||
assert v.bytes() == 1024 ** 3 * 10
|
||||
|
||||
v = base.TokValue.parseString("@10g,digits")[0]
|
||||
assert v.datatype == "digits"
|
||||
g = v.get_generator({})
|
||||
assert g[:100]
|
||||
|
||||
v = base.TokValue.parseString("@10,digits")[0]
|
||||
assert v.unit == "b"
|
||||
assert v.datatype == "digits"
|
||||
|
||||
def test_spec(self):
|
||||
v = base.TokValueGenerate(1, "b", "bytes")
|
||||
assert v.spec() == repr(v) == "@1"
|
||||
|
||||
v = base.TokValueGenerate(1, "k", "bytes")
|
||||
assert v.spec() == repr(v) == "@1k"
|
||||
|
||||
v = base.TokValueGenerate(1, "k", "ascii")
|
||||
assert v.spec() == repr(v) == "@1k,ascii"
|
||||
|
||||
v = base.TokValueGenerate(1, "b", "ascii")
|
||||
assert v.spec() == repr(v) == "@1,ascii"
|
||||
|
||||
def test_freeze(self):
|
||||
v = base.TokValueGenerate(100, "b", "ascii")
|
||||
f = v.freeze(language.Settings())
|
||||
assert len(f.val) == 100
|
||||
|
||||
|
||||
class TestTokValueFile:
|
||||
|
||||
def test_file_value(self):
|
||||
v = base.TokValue.parseString("<'one two'")[0]
|
||||
assert str(v)
|
||||
assert v.path == "one two"
|
||||
|
||||
v = base.TokValue.parseString("<path")[0]
|
||||
assert v.path == "path"
|
||||
|
||||
def test_access_control(self):
|
||||
v = base.TokValue.parseString("<path")[0]
|
||||
with tutils.tmpdir() as t:
|
||||
p = os.path.join(t, "path")
|
||||
with open(p, "wb") as f:
|
||||
f.write("x" * 10000)
|
||||
|
||||
assert v.get_generator(language.Settings(staticdir=t))
|
||||
|
||||
v = base.TokValue.parseString("<path2")[0]
|
||||
tutils.raises(
|
||||
exceptions.FileAccessDenied,
|
||||
v.get_generator,
|
||||
language.Settings(staticdir=t)
|
||||
)
|
||||
tutils.raises(
|
||||
"access disabled",
|
||||
v.get_generator,
|
||||
language.Settings()
|
||||
)
|
||||
|
||||
v = base.TokValue.parseString("</outside")[0]
|
||||
tutils.raises(
|
||||
"outside",
|
||||
v.get_generator,
|
||||
language.Settings(staticdir=t)
|
||||
)
|
||||
|
||||
def test_spec(self):
|
||||
v = base.TokValue.parseString("<'one two'")[0]
|
||||
v2 = base.TokValue.parseString(v.spec())[0]
|
||||
assert v2.path == "one two"
|
||||
|
||||
def test_freeze(self):
|
||||
v = base.TokValue.parseString("<'one two'")[0]
|
||||
v2 = v.freeze({})
|
||||
assert v2.path == v.path
|
||||
|
||||
|
||||
class TestMisc:
|
||||
|
||||
def test_generators(self):
|
||||
v = base.TokValue.parseString("'val'")[0]
|
||||
g = v.get_generator({})
|
||||
assert g[:] == "val"
|
||||
|
||||
def test_value(self):
|
||||
assert base.TokValue.parseString("'val'")[0].val == "val"
|
||||
assert base.TokValue.parseString('"val"')[0].val == "val"
|
||||
assert base.TokValue.parseString('"\'val\'"')[0].val == "'val'"
|
||||
|
||||
def test_value(self):
|
||||
class TT(base.Value):
|
||||
preamble = "m"
|
||||
e = TT.expr()
|
||||
v = e.parseString("m'msg'")[0]
|
||||
assert v.value.val == "msg"
|
||||
|
||||
s = v.spec()
|
||||
assert s == e.parseString(s)[0].spec()
|
||||
|
||||
v = e.parseString("m@100")[0]
|
||||
v2 = v.freeze({})
|
||||
v3 = v2.freeze({})
|
||||
assert v2.value.val == v3.value.val
|
||||
|
||||
def test_fixedlengthvalue(self):
|
||||
class TT(base.FixedLengthValue):
|
||||
preamble = "m"
|
||||
length = 4
|
||||
|
||||
e = TT.expr()
|
||||
assert e.parseString("m@4")
|
||||
tutils.raises("invalid value length", e.parseString, "m@100")
|
||||
tutils.raises("invalid value length", e.parseString, "m@1")
|
||||
|
||||
with tutils.tmpdir() as t:
|
||||
p = os.path.join(t, "path")
|
||||
s = base.Settings(staticdir=t)
|
||||
with open(p, "wb") as f:
|
||||
f.write("a" * 20)
|
||||
v = e.parseString("m<path")[0]
|
||||
tutils.raises("invalid value length", v.values, s)
|
||||
|
||||
p = os.path.join(t, "path")
|
||||
with open(p, "wb") as f:
|
||||
f.write("a" * 4)
|
||||
v = e.parseString("m<path")[0]
|
||||
assert v.values(s)
|
||||
|
||||
|
||||
class TKeyValue(base.KeyValue):
|
||||
preamble = "h"
|
||||
|
||||
def values(self, settings):
|
||||
return [
|
||||
self.key.get_generator(settings),
|
||||
": ",
|
||||
self.value.get_generator(settings),
|
||||
"\r\n",
|
||||
]
|
||||
|
||||
|
||||
class TestKeyValue:
|
||||
|
||||
def test_simple(self):
|
||||
e = TKeyValue.expr()
|
||||
v = e.parseString("h'foo'='bar'")[0]
|
||||
assert v.key.val == "foo"
|
||||
assert v.value.val == "bar"
|
||||
|
||||
v2 = e.parseString(v.spec())[0]
|
||||
assert v2.key.val == v.key.val
|
||||
assert v2.value.val == v.value.val
|
||||
|
||||
s = v.spec()
|
||||
assert s == e.parseString(s)[0].spec()
|
||||
|
||||
def test_freeze(self):
|
||||
e = TKeyValue.expr()
|
||||
v = e.parseString("h@10=@10'")[0]
|
||||
v2 = v.freeze({})
|
||||
v3 = v2.freeze({})
|
||||
assert v2.key.val == v3.key.val
|
||||
assert v2.value.val == v3.value.val
|
||||
|
||||
|
||||
def test_intfield():
|
||||
class TT(base.IntField):
|
||||
preamble = "t"
|
||||
names = {
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
"three": 3
|
||||
}
|
||||
max = 4
|
||||
e = TT.expr()
|
||||
|
||||
v = e.parseString("tone")[0]
|
||||
assert v.value == 1
|
||||
assert v.spec() == "tone"
|
||||
assert v.values(language.Settings())
|
||||
|
||||
v = e.parseString("t1")[0]
|
||||
assert v.value == 1
|
||||
assert v.spec() == "t1"
|
||||
|
||||
v = e.parseString("t4")[0]
|
||||
assert v.value == 4
|
||||
assert v.spec() == "t4"
|
||||
|
||||
tutils.raises("can't exceed", e.parseString, "t5")
|
||||
|
||||
|
||||
def test_options_or_value():
|
||||
class TT(base.OptionsOrValue):
|
||||
options = [
|
||||
"one",
|
||||
"two",
|
||||
"three"
|
||||
]
|
||||
e = TT.expr()
|
||||
assert e.parseString("one")[0].value.val == "one"
|
||||
assert e.parseString("'foo'")[0].value.val == "foo"
|
||||
assert e.parseString("'get'")[0].value.val == "get"
|
||||
|
||||
assert e.parseString("one")[0].spec() == "one"
|
||||
assert e.parseString("'foo'")[0].spec() == "'foo'"
|
||||
|
||||
s = e.parseString("one")[0].spec()
|
||||
assert s == e.parseString(s)[0].spec()
|
||||
|
||||
s = e.parseString("'foo'")[0].spec()
|
||||
assert s == e.parseString(s)[0].spec()
|
||||
|
||||
v = e.parseString("@100")[0]
|
||||
v2 = v.freeze({})
|
||||
v3 = v2.freeze({})
|
||||
assert v2.value.val == v3.value.val
|
||||
|
||||
|
||||
def test_integer():
|
||||
e = base.Integer.expr()
|
||||
v = e.parseString("200")[0]
|
||||
assert v.string() == "200"
|
||||
assert v.spec() == "200"
|
||||
|
||||
assert v.freeze({}).value == v.value
|
||||
|
||||
class BInt(base.Integer):
|
||||
bounds = (1, 5)
|
||||
|
||||
tutils.raises("must be between", BInt, 0)
|
||||
tutils.raises("must be between", BInt, 6)
|
||||
assert BInt(5)
|
||||
assert BInt(1)
|
||||
assert BInt(3)
|
||||
|
||||
|
||||
class TBoolean(base.Boolean):
|
||||
name = "test"
|
||||
|
||||
|
||||
def test_unique_name():
|
||||
b = TBoolean(True)
|
||||
assert b.unique_name
|
||||
|
||||
|
||||
class test_boolean():
|
||||
e = TBoolean.expr()
|
||||
assert e.parseString("test")[0].value
|
||||
assert not e.parseString("-test")[0].value
|
||||
|
||||
def roundtrip(s):
|
||||
e = TBoolean.expr()
|
||||
s2 = e.parseString(s)[0].spec()
|
||||
v1 = e.parseString(s)[0].value
|
||||
v2 = e.parseString(s2)[0].value
|
||||
assert s == s2
|
||||
assert v1 == v2
|
||||
|
||||
roundtrip("test")
|
||||
roundtrip("-test")
|
42
pathod/test/test_language_generators.py
Normal file
42
pathod/test/test_language_generators.py
Normal file
@ -0,0 +1,42 @@
|
||||
import os
|
||||
|
||||
from libpathod.language import generators
|
||||
import tutils
|
||||
|
||||
|
||||
def test_randomgenerator():
|
||||
g = generators.RandomGenerator("bytes", 100)
|
||||
assert repr(g)
|
||||
assert len(g[:10]) == 10
|
||||
assert len(g[1:10]) == 9
|
||||
assert len(g[:1000]) == 100
|
||||
assert len(g[1000:1001]) == 0
|
||||
assert g[0]
|
||||
|
||||
|
||||
def test_filegenerator():
|
||||
with tutils.tmpdir() as t:
|
||||
path = os.path.join(t, "foo")
|
||||
f = open(path, "wb")
|
||||
f.write("x" * 10000)
|
||||
f.close()
|
||||
g = generators.FileGenerator(path)
|
||||
assert len(g) == 10000
|
||||
assert g[0] == "x"
|
||||
assert g[-1] == "x"
|
||||
assert g[0:5] == "xxxxx"
|
||||
assert repr(g)
|
||||
# remove all references to FileGenerator instance to close the file
|
||||
# handle.
|
||||
del g
|
||||
|
||||
|
||||
def test_transform_generator():
|
||||
def trans(offset, data):
|
||||
return "a" * len(data)
|
||||
g = "one"
|
||||
t = generators.TransformGenerator(g, trans)
|
||||
assert len(t) == len(g)
|
||||
assert t[0] == "a"
|
||||
assert t[:] == "a" * len(g)
|
||||
assert repr(t)
|
358
pathod/test/test_language_http.py
Normal file
358
pathod/test/test_language_http.py
Normal file
@ -0,0 +1,358 @@
|
||||
import cStringIO
|
||||
|
||||
from libpathod import language
|
||||
from libpathod.language import http, base
|
||||
import tutils
|
||||
|
||||
|
||||
def parse_request(s):
|
||||
return language.parse_pathoc(s).next()
|
||||
|
||||
|
||||
def test_make_error_response():
|
||||
d = cStringIO.StringIO()
|
||||
s = http.make_error_response("foo")
|
||||
language.serve(s, d, {})
|
||||
|
||||
|
||||
class TestRequest:
|
||||
|
||||
def test_nonascii(self):
|
||||
tutils.raises("ascii", parse_request, "get:\xf0")
|
||||
|
||||
def test_err(self):
|
||||
tutils.raises(language.ParseException, parse_request, 'GET')
|
||||
|
||||
def test_simple(self):
|
||||
r = parse_request('GET:"/foo"')
|
||||
assert r.method.string() == "GET"
|
||||
assert r.path.string() == "/foo"
|
||||
r = parse_request('GET:/foo')
|
||||
assert r.path.string() == "/foo"
|
||||
r = parse_request('GET:@1k')
|
||||
assert len(r.path.string()) == 1024
|
||||
|
||||
def test_multiple(self):
|
||||
r = list(language.parse_pathoc("GET:/ PUT:/"))
|
||||
assert r[0].method.string() == "GET"
|
||||
assert r[1].method.string() == "PUT"
|
||||
assert len(r) == 2
|
||||
|
||||
l = """
|
||||
GET
|
||||
"/foo"
|
||||
ir,@1
|
||||
|
||||
PUT
|
||||
|
||||
"/foo
|
||||
|
||||
|
||||
|
||||
bar"
|
||||
|
||||
ir,@1
|
||||
"""
|
||||
r = list(language.parse_pathoc(l))
|
||||
assert len(r) == 2
|
||||
assert r[0].method.string() == "GET"
|
||||
assert r[1].method.string() == "PUT"
|
||||
|
||||
l = """
|
||||
get:"http://localhost:9999/p/200":ir,@1
|
||||
get:"http://localhost:9999/p/200":ir,@2
|
||||
"""
|
||||
r = list(language.parse_pathoc(l))
|
||||
assert len(r) == 2
|
||||
assert r[0].method.string() == "GET"
|
||||
assert r[1].method.string() == "GET"
|
||||
|
||||
def test_nested_response(self):
|
||||
l = "get:/p:s'200'"
|
||||
r = list(language.parse_pathoc(l))
|
||||
assert len(r) == 1
|
||||
assert len(r[0].tokens) == 3
|
||||
assert isinstance(r[0].tokens[2], http.NestedResponse)
|
||||
assert r[0].values({})
|
||||
|
||||
def test_render(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = parse_request("GET:'/foo'")
|
||||
assert language.serve(
|
||||
r,
|
||||
s,
|
||||
language.Settings(request_host="foo.com")
|
||||
)
|
||||
|
||||
def test_multiline(self):
|
||||
l = """
|
||||
GET
|
||||
"/foo"
|
||||
ir,@1
|
||||
"""
|
||||
r = parse_request(l)
|
||||
assert r.method.string() == "GET"
|
||||
assert r.path.string() == "/foo"
|
||||
assert r.actions
|
||||
|
||||
l = """
|
||||
GET
|
||||
|
||||
"/foo
|
||||
|
||||
|
||||
|
||||
bar"
|
||||
|
||||
ir,@1
|
||||
"""
|
||||
r = parse_request(l)
|
||||
assert r.method.string() == "GET"
|
||||
assert r.path.string().endswith("bar")
|
||||
assert r.actions
|
||||
|
||||
def test_spec(self):
|
||||
def rt(s):
|
||||
s = parse_request(s).spec()
|
||||
assert parse_request(s).spec() == s
|
||||
rt("get:/foo")
|
||||
rt("get:/foo:da")
|
||||
|
||||
def test_freeze(self):
|
||||
r = parse_request("GET:/:b@100").freeze(language.Settings())
|
||||
assert len(r.spec()) > 100
|
||||
|
||||
def test_path_generator(self):
|
||||
r = parse_request("GET:@100").freeze(language.Settings())
|
||||
assert len(r.spec()) > 100
|
||||
|
||||
def test_websocket(self):
|
||||
r = parse_request('ws:/path/')
|
||||
res = r.resolve(language.Settings())
|
||||
assert res.method.string().lower() == "get"
|
||||
assert res.tok(http.Path).value.val == "/path/"
|
||||
assert res.tok(http.Method).value.val.lower() == "get"
|
||||
assert http.get_header("Upgrade", res.headers).value.val == "websocket"
|
||||
|
||||
r = parse_request('ws:put:/path/')
|
||||
res = r.resolve(language.Settings())
|
||||
assert r.method.string().lower() == "put"
|
||||
assert res.tok(http.Path).value.val == "/path/"
|
||||
assert res.tok(http.Method).value.val.lower() == "put"
|
||||
assert http.get_header("Upgrade", res.headers).value.val == "websocket"
|
||||
|
||||
|
||||
class TestResponse:
|
||||
|
||||
def dummy_response(self):
|
||||
return language.parse_pathod("400'msg'").next()
|
||||
|
||||
def test_response(self):
|
||||
r = language.parse_pathod("400:m'msg'").next()
|
||||
assert r.status_code.string() == "400"
|
||||
assert r.reason.string() == "msg"
|
||||
|
||||
r = language.parse_pathod("400:m'msg':b@100b").next()
|
||||
assert r.reason.string() == "msg"
|
||||
assert r.body.values({})
|
||||
assert str(r)
|
||||
|
||||
r = language.parse_pathod("200").next()
|
||||
assert r.status_code.string() == "200"
|
||||
assert not r.reason
|
||||
assert "OK" in [i[:] for i in r.preamble({})]
|
||||
|
||||
def test_render(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = language.parse_pathod("400:m'msg'").next()
|
||||
assert language.serve(r, s, {})
|
||||
|
||||
r = language.parse_pathod("400:p0,100:dr").next()
|
||||
assert "p0" in r.spec()
|
||||
s = r.preview_safe()
|
||||
assert "p0" not in s.spec()
|
||||
|
||||
def test_raw(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = language.parse_pathod("400:b'foo'").next()
|
||||
language.serve(r, s, {})
|
||||
v = s.getvalue()
|
||||
assert "Content-Length" in v
|
||||
|
||||
s = cStringIO.StringIO()
|
||||
r = language.parse_pathod("400:b'foo':r").next()
|
||||
language.serve(r, s, {})
|
||||
v = s.getvalue()
|
||||
assert "Content-Length" not in v
|
||||
|
||||
def test_length(self):
|
||||
def testlen(x):
|
||||
s = cStringIO.StringIO()
|
||||
x = x.next()
|
||||
language.serve(x, s, language.Settings())
|
||||
assert x.length(language.Settings()) == len(s.getvalue())
|
||||
testlen(language.parse_pathod("400:m'msg':r"))
|
||||
testlen(language.parse_pathod("400:m'msg':h'foo'='bar':r"))
|
||||
testlen(language.parse_pathod("400:m'msg':h'foo'='bar':b@100b:r"))
|
||||
|
||||
def test_maximum_length(self):
|
||||
def testlen(x):
|
||||
x = x.next()
|
||||
s = cStringIO.StringIO()
|
||||
m = x.maximum_length({})
|
||||
language.serve(x, s, {})
|
||||
assert m >= len(s.getvalue())
|
||||
|
||||
r = language.parse_pathod("400:m'msg':b@100:d0")
|
||||
testlen(r)
|
||||
|
||||
r = language.parse_pathod("400:m'msg':b@100:d0:i0,'foo'")
|
||||
testlen(r)
|
||||
|
||||
r = language.parse_pathod("400:m'msg':b@100:d0:i0,'foo'")
|
||||
testlen(r)
|
||||
|
||||
def test_parse_err(self):
|
||||
tutils.raises(
|
||||
language.ParseException, language.parse_pathod, "400:msg,b:"
|
||||
)
|
||||
try:
|
||||
language.parse_pathod("400'msg':b:")
|
||||
except language.ParseException as v:
|
||||
assert v.marked()
|
||||
assert str(v)
|
||||
|
||||
def test_nonascii(self):
|
||||
tutils.raises("ascii", language.parse_pathod, "foo:b\xf0")
|
||||
|
||||
def test_parse_header(self):
|
||||
r = language.parse_pathod('400:h"foo"="bar"').next()
|
||||
assert http.get_header("foo", r.headers)
|
||||
|
||||
def test_parse_pause_before(self):
|
||||
r = language.parse_pathod("400:p0,10").next()
|
||||
assert r.actions[0].spec() == "p0,10"
|
||||
|
||||
def test_parse_pause_after(self):
|
||||
r = language.parse_pathod("400:pa,10").next()
|
||||
assert r.actions[0].spec() == "pa,10"
|
||||
|
||||
def test_parse_pause_random(self):
|
||||
r = language.parse_pathod("400:pr,10").next()
|
||||
assert r.actions[0].spec() == "pr,10"
|
||||
|
||||
def test_parse_stress(self):
|
||||
# While larger values are known to work on linux, len() technically
|
||||
# returns an int and a python 2.7 int on windows has 32bit precision.
|
||||
# Therefore, we should keep the body length < 2147483647 bytes in our
|
||||
# tests.
|
||||
r = language.parse_pathod("400:b@1g").next()
|
||||
assert r.length({})
|
||||
|
||||
def test_spec(self):
|
||||
def rt(s):
|
||||
s = language.parse_pathod(s).next().spec()
|
||||
assert language.parse_pathod(s).next().spec() == s
|
||||
rt("400:b@100g")
|
||||
rt("400")
|
||||
rt("400:da")
|
||||
|
||||
def test_websockets(self):
|
||||
r = language.parse_pathod("ws").next()
|
||||
tutils.raises("no websocket key", r.resolve, language.Settings())
|
||||
res = r.resolve(language.Settings(websocket_key="foo"))
|
||||
assert res.status_code.string() == "101"
|
||||
|
||||
|
||||
def test_ctype_shortcut():
|
||||
e = http.ShortcutContentType.expr()
|
||||
v = e.parseString("c'foo'")[0]
|
||||
assert v.key.val == "Content-Type"
|
||||
assert v.value.val == "foo"
|
||||
|
||||
s = v.spec()
|
||||
assert s == e.parseString(s)[0].spec()
|
||||
|
||||
e = http.ShortcutContentType.expr()
|
||||
v = e.parseString("c@100")[0]
|
||||
v2 = v.freeze({})
|
||||
v3 = v2.freeze({})
|
||||
assert v2.value.val == v3.value.val
|
||||
|
||||
|
||||
def test_location_shortcut():
|
||||
e = http.ShortcutLocation.expr()
|
||||
v = e.parseString("l'foo'")[0]
|
||||
assert v.key.val == "Location"
|
||||
assert v.value.val == "foo"
|
||||
|
||||
s = v.spec()
|
||||
assert s == e.parseString(s)[0].spec()
|
||||
|
||||
e = http.ShortcutLocation.expr()
|
||||
v = e.parseString("l@100")[0]
|
||||
v2 = v.freeze({})
|
||||
v3 = v2.freeze({})
|
||||
assert v2.value.val == v3.value.val
|
||||
|
||||
|
||||
def test_shortcuts():
|
||||
assert language.parse_pathod(
|
||||
"400:c'foo'").next().headers[0].key.val == "Content-Type"
|
||||
assert language.parse_pathod(
|
||||
"400:l'foo'").next().headers[0].key.val == "Location"
|
||||
|
||||
assert "Android" in tutils.render(parse_request("get:/:ua"))
|
||||
assert "User-Agent" in tutils.render(parse_request("get:/:ua"))
|
||||
|
||||
|
||||
def test_user_agent():
|
||||
e = http.ShortcutUserAgent.expr()
|
||||
v = e.parseString("ua")[0]
|
||||
assert "Android" in v.string()
|
||||
|
||||
e = http.ShortcutUserAgent.expr()
|
||||
v = e.parseString("u'a'")[0]
|
||||
assert "Android" not in v.string()
|
||||
|
||||
v = e.parseString("u@100'")[0]
|
||||
assert len(str(v.freeze({}).value)) > 100
|
||||
v2 = v.freeze({})
|
||||
v3 = v2.freeze({})
|
||||
assert v2.value.val == v3.value.val
|
||||
|
||||
|
||||
def test_nested_response():
|
||||
e = http.NestedResponse.expr()
|
||||
v = e.parseString("s'200'")[0]
|
||||
assert v.value.val == "200"
|
||||
tutils.raises(
|
||||
language.ParseException,
|
||||
e.parseString,
|
||||
"s'foo'"
|
||||
)
|
||||
|
||||
v = e.parseString('s"200:b@1"')[0]
|
||||
assert "@1" in v.spec()
|
||||
f = v.freeze({})
|
||||
assert "@1" not in f.spec()
|
||||
|
||||
|
||||
def test_nested_response_freeze():
|
||||
e = http.NestedResponse(
|
||||
base.TokValueLiteral(
|
||||
"200:b'foo':i10,'\\x27'".encode(
|
||||
"string_escape"
|
||||
)
|
||||
)
|
||||
)
|
||||
assert e.freeze({})
|
||||
assert e.values({})
|
||||
|
||||
|
||||
def test_unique_components():
|
||||
tutils.raises(
|
||||
"multiple body clauses",
|
||||
language.parse_pathod,
|
||||
"400:b@1:b@1"
|
||||
)
|
233
pathod/test/test_language_http2.py
Normal file
233
pathod/test/test_language_http2.py
Normal file
@ -0,0 +1,233 @@
|
||||
import cStringIO
|
||||
|
||||
import netlib
|
||||
from netlib import tcp
|
||||
from netlib.http import user_agents
|
||||
|
||||
from libpathod import language
|
||||
from libpathod.language import http2, base
|
||||
import tutils
|
||||
|
||||
|
||||
def parse_request(s):
|
||||
return language.parse_pathoc(s, True).next()
|
||||
|
||||
|
||||
def parse_response(s):
|
||||
return language.parse_pathod(s, True).next()
|
||||
|
||||
|
||||
def default_settings():
|
||||
return language.Settings(
|
||||
request_host="foo.com",
|
||||
protocol=netlib.http.http2.HTTP2Protocol(tcp.TCPClient(('localhost', 1234)))
|
||||
)
|
||||
|
||||
|
||||
def test_make_error_response():
|
||||
d = cStringIO.StringIO()
|
||||
s = http2.make_error_response("foo", "bar")
|
||||
language.serve(s, d, default_settings())
|
||||
|
||||
|
||||
class TestRequest:
|
||||
|
||||
def test_cached_values(self):
|
||||
req = parse_request("get:/")
|
||||
req_id = id(req)
|
||||
assert req_id == id(req.resolve(default_settings()))
|
||||
assert req.values(default_settings()) == req.values(default_settings())
|
||||
|
||||
def test_nonascii(self):
|
||||
tutils.raises("ascii", parse_request, "get:\xf0")
|
||||
|
||||
def test_err(self):
|
||||
tutils.raises(language.ParseException, parse_request, 'GET')
|
||||
|
||||
def test_simple(self):
|
||||
r = parse_request('GET:"/foo"')
|
||||
assert r.method.string() == "GET"
|
||||
assert r.path.string() == "/foo"
|
||||
r = parse_request('GET:/foo')
|
||||
assert r.path.string() == "/foo"
|
||||
|
||||
def test_multiple(self):
|
||||
r = list(language.parse_pathoc("GET:/ PUT:/"))
|
||||
assert r[0].method.string() == "GET"
|
||||
assert r[1].method.string() == "PUT"
|
||||
assert len(r) == 2
|
||||
|
||||
l = """
|
||||
GET
|
||||
"/foo"
|
||||
|
||||
PUT
|
||||
|
||||
"/foo
|
||||
|
||||
|
||||
|
||||
bar"
|
||||
"""
|
||||
r = list(language.parse_pathoc(l, True))
|
||||
assert len(r) == 2
|
||||
assert r[0].method.string() == "GET"
|
||||
assert r[1].method.string() == "PUT"
|
||||
|
||||
l = """
|
||||
get:"http://localhost:9999/p/200"
|
||||
get:"http://localhost:9999/p/200"
|
||||
"""
|
||||
r = list(language.parse_pathoc(l, True))
|
||||
assert len(r) == 2
|
||||
assert r[0].method.string() == "GET"
|
||||
assert r[1].method.string() == "GET"
|
||||
|
||||
def test_render_simple(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = parse_request("GET:'/foo'")
|
||||
assert language.serve(
|
||||
r,
|
||||
s,
|
||||
default_settings(),
|
||||
)
|
||||
|
||||
def test_raw_content_length(self):
|
||||
r = parse_request('GET:/:r')
|
||||
assert len(r.headers) == 0
|
||||
|
||||
r = parse_request('GET:/:r:b"foobar"')
|
||||
assert len(r.headers) == 0
|
||||
|
||||
r = parse_request('GET:/')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("content-length", "0")
|
||||
|
||||
r = parse_request('GET:/:b"foobar"')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("content-length", "6")
|
||||
|
||||
r = parse_request('GET:/:b"foobar":h"content-length"="42"')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("content-length", "42")
|
||||
|
||||
r = parse_request('GET:/:r:b"foobar":h"content-length"="42"')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("content-length", "42")
|
||||
|
||||
def test_content_type(self):
|
||||
r = parse_request('GET:/:r:c"foobar"')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("content-type", "foobar")
|
||||
|
||||
def test_user_agent(self):
|
||||
r = parse_request('GET:/:r:ua')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("user-agent", user_agents.get_by_shortcut('a')[2])
|
||||
|
||||
def test_render_with_headers(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = parse_request('GET:/foo:h"foo"="bar"')
|
||||
assert language.serve(
|
||||
r,
|
||||
s,
|
||||
default_settings(),
|
||||
)
|
||||
|
||||
def test_nested_response(self):
|
||||
l = "get:/p/:s'200'"
|
||||
r = parse_request(l)
|
||||
assert len(r.tokens) == 3
|
||||
assert isinstance(r.tokens[2], http2.NestedResponse)
|
||||
assert r.values(default_settings())
|
||||
|
||||
|
||||
def test_render_with_body(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = parse_request("GET:'/foo':bfoobar")
|
||||
assert language.serve(
|
||||
r,
|
||||
s,
|
||||
default_settings(),
|
||||
)
|
||||
|
||||
def test_spec(self):
|
||||
def rt(s):
|
||||
s = parse_request(s).spec()
|
||||
assert parse_request(s).spec() == s
|
||||
rt("get:/foo")
|
||||
|
||||
|
||||
class TestResponse:
|
||||
|
||||
def test_cached_values(self):
|
||||
res = parse_response("200")
|
||||
res_id = id(res)
|
||||
assert res_id == id(res.resolve(default_settings()))
|
||||
assert res.values(default_settings()) == res.values(default_settings())
|
||||
|
||||
def test_nonascii(self):
|
||||
tutils.raises("ascii", parse_response, "200:\xf0")
|
||||
|
||||
def test_err(self):
|
||||
tutils.raises(language.ParseException, parse_response, 'GET:/')
|
||||
|
||||
def test_raw_content_length(self):
|
||||
r = parse_response('200:r')
|
||||
assert len(r.headers) == 0
|
||||
|
||||
r = parse_response('200')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("content-length", "0")
|
||||
|
||||
def test_content_type(self):
|
||||
r = parse_response('200:r:c"foobar"')
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("content-type", "foobar")
|
||||
|
||||
def test_simple(self):
|
||||
r = parse_response('200:r:h"foo"="bar"')
|
||||
assert r.status_code.string() == "200"
|
||||
assert len(r.headers) == 1
|
||||
assert r.headers[0].values(default_settings()) == ("foo", "bar")
|
||||
assert r.body is None
|
||||
|
||||
r = parse_response('200:r:h"foo"="bar":bfoobar:h"bla"="fasel"')
|
||||
assert r.status_code.string() == "200"
|
||||
assert len(r.headers) == 2
|
||||
assert r.headers[0].values(default_settings()) == ("foo", "bar")
|
||||
assert r.headers[1].values(default_settings()) == ("bla", "fasel")
|
||||
assert r.body.string() == "foobar"
|
||||
|
||||
def test_render_simple(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = parse_response('200')
|
||||
assert language.serve(
|
||||
r,
|
||||
s,
|
||||
default_settings(),
|
||||
)
|
||||
|
||||
def test_render_with_headers(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = parse_response('200:h"foo"="bar"')
|
||||
assert language.serve(
|
||||
r,
|
||||
s,
|
||||
default_settings(),
|
||||
)
|
||||
|
||||
def test_render_with_body(self):
|
||||
s = cStringIO.StringIO()
|
||||
r = parse_response('200:bfoobar')
|
||||
assert language.serve(
|
||||
r,
|
||||
s,
|
||||
default_settings(),
|
||||
)
|
||||
|
||||
def test_spec(self):
|
||||
def rt(s):
|
||||
s = parse_response(s).spec()
|
||||
assert parse_response(s).spec() == s
|
||||
rt("200:bfoobar")
|
142
pathod/test/test_language_websocket.py
Normal file
142
pathod/test/test_language_websocket.py
Normal file
@ -0,0 +1,142 @@
|
||||
|
||||
from libpathod import language
|
||||
from libpathod.language import websockets
|
||||
import netlib.websockets
|
||||
import tutils
|
||||
|
||||
|
||||
def parse_request(s):
|
||||
return language.parse_pathoc(s).next()
|
||||
|
||||
|
||||
class TestWebsocketFrame:
|
||||
|
||||
def _test_messages(self, specs, message_klass):
|
||||
for i in specs:
|
||||
wf = parse_request(i)
|
||||
assert isinstance(wf, message_klass)
|
||||
assert wf
|
||||
assert wf.values(language.Settings())
|
||||
assert wf.resolve(language.Settings())
|
||||
|
||||
spec = wf.spec()
|
||||
wf2 = parse_request(spec)
|
||||
assert wf2.spec() == spec
|
||||
|
||||
def test_server_values(self):
|
||||
specs = [
|
||||
"wf",
|
||||
"wf:dr",
|
||||
"wf:b'foo'",
|
||||
"wf:mask:r'foo'",
|
||||
"wf:l1024:b'foo'",
|
||||
"wf:cbinary",
|
||||
"wf:c1",
|
||||
"wf:mask:knone",
|
||||
"wf:fin",
|
||||
"wf:fin:rsv1:rsv2:rsv3:mask",
|
||||
"wf:-fin:-rsv1:-rsv2:-rsv3:-mask",
|
||||
"wf:k@4",
|
||||
"wf:x10",
|
||||
]
|
||||
self._test_messages(specs, websockets.WebsocketFrame)
|
||||
|
||||
def test_parse_websocket_frames(self):
|
||||
wf = language.parse_websocket_frame("wf:x10")
|
||||
assert len(list(wf)) == 10
|
||||
tutils.raises(
|
||||
language.ParseException,
|
||||
language.parse_websocket_frame,
|
||||
"wf:x"
|
||||
)
|
||||
|
||||
def test_client_values(self):
|
||||
specs = [
|
||||
"wf:f'wf'",
|
||||
]
|
||||
self._test_messages(specs, websockets.WebsocketClientFrame)
|
||||
|
||||
def test_nested_frame(self):
|
||||
wf = parse_request("wf:f'wf'")
|
||||
assert wf.nested_frame
|
||||
|
||||
def test_flags(self):
|
||||
wf = parse_request("wf:fin:mask:rsv1:rsv2:rsv3")
|
||||
frm = netlib.websockets.Frame.from_bytes(tutils.render(wf))
|
||||
assert frm.header.fin
|
||||
assert frm.header.mask
|
||||
assert frm.header.rsv1
|
||||
assert frm.header.rsv2
|
||||
assert frm.header.rsv3
|
||||
|
||||
wf = parse_request("wf:-fin:-mask:-rsv1:-rsv2:-rsv3")
|
||||
frm = netlib.websockets.Frame.from_bytes(tutils.render(wf))
|
||||
assert not frm.header.fin
|
||||
assert not frm.header.mask
|
||||
assert not frm.header.rsv1
|
||||
assert not frm.header.rsv2
|
||||
assert not frm.header.rsv3
|
||||
|
||||
def fr(self, spec, **kwargs):
|
||||
settings = language.base.Settings(**kwargs)
|
||||
wf = parse_request(spec)
|
||||
return netlib.websockets.Frame.from_bytes(tutils.render(wf, settings))
|
||||
|
||||
def test_construction(self):
|
||||
assert self.fr("wf:c1").header.opcode == 1
|
||||
assert self.fr("wf:c0").header.opcode == 0
|
||||
assert self.fr("wf:cbinary").header.opcode ==\
|
||||
netlib.websockets.OPCODE.BINARY
|
||||
assert self.fr("wf:ctext").header.opcode ==\
|
||||
netlib.websockets.OPCODE.TEXT
|
||||
|
||||
def test_rawbody(self):
|
||||
frm = self.fr("wf:mask:r'foo'")
|
||||
assert len(frm.payload) == 3
|
||||
assert frm.payload != "foo"
|
||||
|
||||
assert self.fr("wf:r'foo'").payload == "foo"
|
||||
|
||||
def test_construction(self):
|
||||
# Simple server frame
|
||||
frm = self.fr("wf:b'foo'")
|
||||
assert not frm.header.mask
|
||||
assert not frm.header.masking_key
|
||||
|
||||
# Simple client frame
|
||||
frm = self.fr("wf:b'foo'", is_client=True)
|
||||
assert frm.header.mask
|
||||
assert frm.header.masking_key
|
||||
frm = self.fr("wf:b'foo':k'abcd'", is_client=True)
|
||||
assert frm.header.mask
|
||||
assert frm.header.masking_key == 'abcd'
|
||||
|
||||
# Server frame, mask explicitly set
|
||||
frm = self.fr("wf:b'foo':mask")
|
||||
assert frm.header.mask
|
||||
assert frm.header.masking_key
|
||||
frm = self.fr("wf:b'foo':k'abcd'")
|
||||
assert frm.header.mask
|
||||
assert frm.header.masking_key == 'abcd'
|
||||
|
||||
# Client frame, mask explicitly unset
|
||||
frm = self.fr("wf:b'foo':-mask", is_client=True)
|
||||
assert not frm.header.mask
|
||||
assert not frm.header.masking_key
|
||||
|
||||
frm = self.fr("wf:b'foo':-mask:k'abcd'", is_client=True)
|
||||
assert not frm.header.mask
|
||||
# We're reading back a corrupted frame - the first 3 characters of the
|
||||
# mask is mis-interpreted as the payload
|
||||
assert frm.payload == "abc"
|
||||
|
||||
def test_knone(self):
|
||||
with tutils.raises("expected 4 bytes"):
|
||||
self.fr("wf:b'foo':mask:knone")
|
||||
|
||||
def test_length(self):
|
||||
assert self.fr("wf:l3:b'foo'").header.payload_length == 3
|
||||
frm = self.fr("wf:l2:b'foo'")
|
||||
assert frm.header.payload_length == 2
|
||||
assert frm.payload == "fo"
|
||||
tutils.raises("expected 1024 bytes", self.fr, "wf:l1024:b'foo'")
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user