pax_global_header00006660000000000000000000000064136173660210014517gustar00rootroot0000000000000052 comment=354297a066e6a2ba26c2a280ca15863ea68d35d9 httptools-0.1.1/000077500000000000000000000000001361736602100135565ustar00rootroot00000000000000httptools-0.1.1/.github/000077500000000000000000000000001361736602100151165ustar00rootroot00000000000000httptools-0.1.1/.github/workflows/000077500000000000000000000000001361736602100171535ustar00rootroot00000000000000httptools-0.1.1/.github/workflows/build-manylinux-wheels.sh000077500000000000000000000012751361736602100241250ustar00rootroot00000000000000#!/bin/bash set -e -x PY_MAJOR=${PYTHON_VERSION%%.*} PY_MINOR=${PYTHON_VERSION#*.} ML_PYTHON_VERSION="cp${PY_MAJOR}${PY_MINOR}-cp${PY_MAJOR}${PY_MINOR}" if [ "${PY_MAJOR}" -lt "4" -a "${PY_MINOR}" -lt "8" ]; then ML_PYTHON_VERSION+="m" fi # Compile wheels PYTHON="/opt/python/${ML_PYTHON_VERSION}/bin/python" PIP="/opt/python/${ML_PYTHON_VERSION}/bin/pip" "${PIP}" install --upgrade setuptools pip wheel~=0.31.1 cd "${GITHUB_WORKSPACE}" make clean "${PYTHON}" setup.py bdist_wheel # Bundle external shared libraries into the wheels. for whl in "${GITHUB_WORKSPACE}"/dist/*.whl; do auditwheel repair $whl -w "${GITHUB_WORKSPACE}"/dist/ rm "${GITHUB_WORKSPACE}"/dist/*-linux_*.whl done httptools-0.1.1/.github/workflows/release-trigger.yml000066400000000000000000000013351361736602100227610ustar00rootroot00000000000000name: Trigger Release on: pull_request_review: types: [submitted] jobs: check-review: runs-on: ubuntu-latest steps: - name: Validate release PR uses: edgedb/action-release/validate-pr@master id: release continue-on-error: true with: github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} version_file: httptools/_version.py version_line_pattern: | __version__\s*=\s*(?:['"])([[:PEP440:]])(?:['"]) - name: Trigger release uses: edgedb/action-release/trigger@master if: steps.release.outputs.version != 0 with: github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} release_validation_check: "validate-release-request" httptools-0.1.1/.github/workflows/release.yml000066400000000000000000000107741361736602100213270ustar00rootroot00000000000000name: Release on: pull_request: branches: - "master" - "ci" - "[0-9]+.[0-9x]+*" paths: - "httptools/_version.py" jobs: validate-release-request: runs-on: ubuntu-latest steps: - name: Validate release PR uses: edgedb/action-release/validate-pr@master id: checkver with: github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} version_file: httptools/_version.py version_line_pattern: | __version__\s*=\s*(?:['"])([[:PEP440:]])(?:['"]) - name: Stop if not approved if: steps.checkver.outputs.approved != 'true' run: | echo ::error::PR is not approved yet. exit 1 - name: Store release version for later use env: VERSION: ${{ steps.checkver.outputs.version }} run: | mkdir -p dist/ echo "${VERSION}" > dist/VERSION - uses: actions/upload-artifact@v1 with: name: dist path: dist/ build-sdist: needs: validate-release-request runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 with: fetch-depth: 50 submodules: true - name: Set up Python 3.7 uses: actions/setup-python@v1 with: python-version: 3.7 - name: Build source distribution run: | pip install -U setuptools wheel pip python setup.py sdist - uses: actions/upload-artifact@v1 with: name: dist path: dist/ build-wheels: needs: validate-release-request runs-on: ${{ matrix.os }} strategy: matrix: python-version: [3.5, 3.6, 3.7, 3.8] os: [ubuntu-16.04, macos-latest, windows-latest] exclude: # Python 3.5 is unable to properly # find the recent VS tooling # https://bugs.python.org/issue30389 - os: windows-latest python-version: 3.5 steps: - uses: actions/checkout@v1 with: fetch-depth: 50 submodules: true - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Python Deps run: | python -m pip install --upgrade setuptools pip wheel - name: Build Wheels (linux) if: startsWith(matrix.os, 'ubuntu') uses: docker://quay.io/pypa/manylinux1_x86_64 env: PYTHON_VERSION: ${{ matrix.python-version }} with: entrypoint: /github/workspace/.github/workflows/build-manylinux-wheels.sh - name: Build Wheels (non-linux) if: "!startsWith(matrix.os, 'ubuntu')" run: | make clean python setup.py bdist_wheel - name: Test Wheels if: | !startsWith(matrix.os, 'windows') && !contains(github.event.pull_request.labels.*.name, 'skip wheel tests') run: | pip install --pre httptools -f "file:///${GITHUB_WORKSPACE}/dist" make -C "${GITHUB_WORKSPACE}" testinstalled - uses: actions/upload-artifact@v1 with: name: dist path: dist/ publish: needs: [build-sdist, build-wheels] runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 with: fetch-depth: 5 submodules: false - uses: actions/download-artifact@v1 with: name: dist path: dist/ - name: Extract Release Version id: relver run: | set -e echo ::set-output name=version::$(cat dist/VERSION) rm dist/VERSION - name: Merge and tag the PR uses: edgedb/action-release/merge@master with: github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} ssh_key: ${{ secrets.RELEASE_BOT_SSH_KEY }} gpg_key: ${{ secrets.RELEASE_BOT_GPG_KEY }} gpg_key_id: "5C468778062D87BF!" tag_name: v${{ steps.relver.outputs.version }} - name: Publish Github Release uses: elprans/gh-action-create-release@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: v${{ steps.relver.outputs.version }} release_name: v${{ steps.relver.outputs.version }} target: ${{ github.event.pull_request.base.ref }} body: ${{ github.event.pull_request.body }} draft: true - run: | ls -al dist/ - name: Upload to PyPI uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} # password: ${{ secrets.TEST_PYPI_TOKEN }} # repository_url: https://test.pypi.org/legacy/ httptools-0.1.1/.github/workflows/tests.yml000066400000000000000000000024321361736602100210410ustar00rootroot00000000000000name: Tests on: push: branches: - master - ci pull_request: branches: - master jobs: build: runs-on: ${{ matrix.os }} strategy: max-parallel: 4 matrix: python-version: [3.5, 3.6, 3.7, 3.8] os: [windows-latest, ubuntu-18.04, macos-latest] exclude: # Python 3.5 is unable to properly # find the recent VS tooling # https://bugs.python.org/issue30389 - os: windows-latest python-version: 3.5 steps: - uses: actions/checkout@v1 with: fetch-depth: 50 submodules: true - name: Check if release PR. uses: edgedb/action-release/validate-pr@master continue-on-error: true id: release with: github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} version_file: httptools/_version.py version_line_pattern: | __version__\s*=\s*(?:['"])([[:PEP440:]])(?:['"]) - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 if: steps.release.outputs.version == 0 with: python-version: ${{ matrix.python-version }} - name: Test if: steps.release.outputs.version == 0 run: | pip install -e .[test] python setup.py test httptools-0.1.1/.gitignore000066400000000000000000000004211361736602100155430ustar00rootroot00000000000000*._* *.pyc *.pyo *.ymlc *.ymlc~ *.scssc *.so *.pyd *~ .#* .DS_Store .project .pydevproject .settings .idea /.ropeproject \#*# /pub /test*.py /.local /perf.data* /config_local.yml /build __pycache__/ .d8_history /*.egg /*.egg-info /dist /.pytest_cache /.mypy_cache /.vscode httptools-0.1.1/.gitmodules000066400000000000000000000001551361736602100157340ustar00rootroot00000000000000[submodule "vendor/http-parser"] path = vendor/http-parser url = https://github.com/nodejs/http-parser.git httptools-0.1.1/LICENSE000066400000000000000000000021051361736602100145610ustar00rootroot00000000000000The MIT License Copyright (c) 2015 MagicStack Inc. http://magic.io 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. httptools-0.1.1/MANIFEST.in000066400000000000000000000001161361736602100153120ustar00rootroot00000000000000recursive-include vendor *.c *.h LICENSE* README* include MANIFEST.in LICENSE httptools-0.1.1/Makefile000066400000000000000000000011721361736602100152170ustar00rootroot00000000000000.PHONY: compile release test distclean clean PYTHON ?= python3 ROOT = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) compile: python3 setup.py build_ext --inplace release: compile test python3 setup.py sdist upload test: python3 setup.py test clean: find $(ROOT)/httptools/parser -name '*.c' | xargs rm -f find $(ROOT)/httptools/parser -name '*.html' | xargs rm -f distclean: git --git-dir="$(ROOT)/vendor/http-parser/.git" clean -dfx find $(ROOT)/httptools/parser -name '*.c' | xargs rm -f find $(ROOT)/httptools/parser -name '*.html' | xargs rm -f testinstalled: cd /tmp && $(PYTHON) $(ROOT)/tests/__init__.pyhttptools-0.1.1/README.md000066400000000000000000000051451361736602100150420ustar00rootroot00000000000000![Tests](https://github.com/MagicStack/httptools/workflows/Tests/badge.svg) httptools is a Python binding for the nodejs HTTP parser. The package is available on PyPI: `pip install httptools`. # APIs httptools contains two classes `httptools.HttpRequestParser`, `httptools.HttpResponseParser` and a function for parsing URLs `httptools.parse_url`. See unittests for examples. ```python class HttpRequestParser: def __init__(self, protocol): """HttpRequestParser protocol -- a Python object with the following methods (all optional): - on_message_begin() - on_url(url: bytes) - on_header(name: bytes, value: bytes) - on_headers_complete() - on_body(body: bytes) - on_message_complete() - on_chunk_header() - on_chunk_complete() - on_status(status: bytes) """ def get_http_version(self) -> str: """Return an HTTP protocol version.""" def should_keep_alive(self) -> bool: """Return ``True`` if keep-alive mode is preferred.""" def should_upgrade(self) -> bool: """Return ``True`` if the parsed request is a valid Upgrade request. The method exposes a flag set just before on_headers_complete. Calling this method earlier will only yield `False`. """ def feed_data(self, data: bytes): """Feed data to the parser. Will eventually trigger callbacks on the ``protocol`` object. On HTTP upgrade, this method will raise an ``HttpParserUpgrade`` exception, with its sole argument set to the offset of the non-HTTP data in ``data``. """ def get_method(self) -> bytes: """Return HTTP request method (GET, HEAD, etc)""" class HttpResponseParser: """Has all methods except ``get_method()`` that HttpRequestParser has.""" def get_status_code(self) -> int: """Return the status code of the HTTP response""" def parse_url(url: bytes): """Parse URL strings into a structured Python object. Returns an instance of ``httptools.URL`` class with the following attributes: - schema: bytes - host: bytes - port: int - path: bytes - query: bytes - fragment: bytes - userinfo: bytes """ ``` # Development 1. Clone this repository with `git clone --recursive git@github.com:MagicStack/httptools.git` 2. Create a virtual environment with Python 3.5: `python3.5 -m venv envname` 3. Activate the environment with `source envname/bin/activate` 4. Install Cython with `pip install cython` 5. Run `make` and `make test`. # License MIT. httptools-0.1.1/httptools/000077500000000000000000000000001361736602100156165ustar00rootroot00000000000000httptools-0.1.1/httptools/__init__.py000066400000000000000000000002061361736602100177250ustar00rootroot00000000000000from .parser import parser from .parser import * # NOQA from ._version import __version__ # NOQA __all__ = parser.__all__ # NOQA httptools-0.1.1/httptools/_version.py000066400000000000000000000010771361736602100200210ustar00rootroot00000000000000# This file MUST NOT contain anything but the __version__ assignment. # # When making a release, change the value of __version__ # to an appropriate value, and open a pull request against # the correct branch (master if making a new feature release). # The commit message MUST contain a properly formatted release # log, and the commit must be signed. # # The release automation will: build and test the packages for the # supported platforms, publish the packages on PyPI, merge the PR # to the target branch, create a Git tag pointing to the commit. __version__ = '0.1.1' httptools-0.1.1/httptools/parser/000077500000000000000000000000001361736602100171125ustar00rootroot00000000000000httptools-0.1.1/httptools/parser/.gitignore000066400000000000000000000000201361736602100210720ustar00rootroot00000000000000*.so *.html *.c httptools-0.1.1/httptools/parser/__init__.py000066400000000000000000000001301361736602100212150ustar00rootroot00000000000000from .parser import * from .errors import * __all__ = parser.__all__ + errors.__all__ httptools-0.1.1/httptools/parser/cparser.pxd000066400000000000000000000075411361736602100212750ustar00rootroot00000000000000from libc.stdint cimport uint16_t, uint32_t, uint64_t cdef extern from "../../vendor/http-parser/http_parser.h": ctypedef int (*http_data_cb) (http_parser*, const char *at, size_t length) except -1 ctypedef int (*http_cb) (http_parser*) except -1 struct http_parser: unsigned int type unsigned int flags unsigned int state unsigned int header_state unsigned int index uint32_t nread uint64_t content_length unsigned short http_major unsigned short http_minor unsigned int status_code unsigned int method unsigned int http_errno unsigned int upgrade void *data struct http_parser_settings: http_cb on_message_begin http_data_cb on_url http_data_cb on_status http_data_cb on_header_field http_data_cb on_header_value http_cb on_headers_complete http_data_cb on_body http_cb on_message_complete http_cb on_chunk_header http_cb on_chunk_complete enum http_parser_type: HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH enum http_errno: HPE_OK, HPE_CB_message_begin, HPE_CB_url, HPE_CB_header_field, HPE_CB_header_value, HPE_CB_headers_complete, HPE_CB_body, HPE_CB_message_complete, HPE_CB_status, HPE_CB_chunk_header, HPE_CB_chunk_complete, HPE_INVALID_EOF_STATE, HPE_HEADER_OVERFLOW, HPE_CLOSED_CONNECTION, HPE_INVALID_VERSION, HPE_INVALID_STATUS, HPE_INVALID_METHOD, HPE_INVALID_URL, HPE_INVALID_HOST, HPE_INVALID_PORT, HPE_INVALID_PATH, HPE_INVALID_QUERY_STRING, HPE_INVALID_FRAGMENT, HPE_LF_EXPECTED, HPE_INVALID_HEADER_TOKEN, HPE_INVALID_CONTENT_LENGTH, HPE_INVALID_CHUNK_SIZE, HPE_INVALID_CONSTANT, HPE_INVALID_INTERNAL_STATE, HPE_STRICT, HPE_PAUSED, HPE_UNKNOWN enum flags: F_CHUNKED, F_CONNECTION_KEEP_ALIVE, F_CONNECTION_CLOSE, F_CONNECTION_UPGRADE, F_TRAILING, F_UPGRADE, F_SKIPBODY enum http_method: DELETE, GET, HEAD, POST, PUT, CONNECT, OPTIONS, TRACE, COPY, LOCK, MKCOL, MOVE, PROPFIND, PROPPATCH, SEARCH, UNLOCK, BIND, REBIND, UNBIND, ACL, REPORT, MKACTIVITY, CHECKOUT, MERGE, MSEARCH, NOTIFY, SUBSCRIBE, UNSUBSCRIBE, PATCH, PURGE, MKCALENDAR, LINK, UNLINK void http_parser_init(http_parser *parser, http_parser_type type) size_t http_parser_execute(http_parser *parser, const http_parser_settings *settings, const char *data, size_t len) int http_should_keep_alive(const http_parser *parser) void http_parser_settings_init(http_parser_settings *settings) const char *http_errno_name(http_errno err) const char *http_errno_description(http_errno err) const char *http_method_str(http_method m) # URL Parser enum http_parser_url_fields: UF_SCHEMA = 0, UF_HOST = 1, UF_PORT = 2, UF_PATH = 3, UF_QUERY = 4, UF_FRAGMENT = 5, UF_USERINFO = 6, UF_MAX = 7 struct http_parser_url_field_data: uint16_t off uint16_t len struct http_parser_url: uint16_t field_set uint16_t port http_parser_url_field_data[UF_MAX] field_data void http_parser_url_init(http_parser_url *u) int http_parser_parse_url(const char *buf, size_t buflen, int is_connect, http_parser_url *u) httptools-0.1.1/httptools/parser/errors.py000066400000000000000000000010661361736602100210030ustar00rootroot00000000000000__all__ = ('HttpParserError', 'HttpParserCallbackError', 'HttpParserInvalidStatusError', 'HttpParserInvalidMethodError', 'HttpParserInvalidURLError', 'HttpParserUpgrade') class HttpParserError(Exception): pass class HttpParserCallbackError(HttpParserError): pass class HttpParserInvalidStatusError(HttpParserError): pass class HttpParserInvalidMethodError(HttpParserError): pass class HttpParserInvalidURLError(HttpParserError): pass class HttpParserUpgrade(Exception): pass httptools-0.1.1/httptools/parser/parser.pyx000066400000000000000000000355071361736602100211620ustar00rootroot00000000000000#cython: language_level=3 from __future__ import print_function from cpython.mem cimport PyMem_Malloc, PyMem_Free from cpython cimport PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, \ Py_buffer, PyBytes_AsString from .python cimport PyMemoryView_Check, PyMemoryView_GET_BUFFER from .errors import (HttpParserError, HttpParserCallbackError, HttpParserInvalidStatusError, HttpParserInvalidMethodError, HttpParserInvalidURLError, HttpParserUpgrade) cimport cython from . cimport cparser __all__ = ('HttpRequestParser', 'HttpResponseParser', 'parse_url') @cython.internal cdef class HttpParser: cdef: cparser.http_parser* _cparser cparser.http_parser_settings* _csettings bytes _current_header_name bytes _current_header_value _proto_on_url, _proto_on_status, _proto_on_body, \ _proto_on_header, _proto_on_headers_complete, \ _proto_on_message_complete, _proto_on_chunk_header, \ _proto_on_chunk_complete, _proto_on_message_begin object _last_error Py_buffer py_buf def __cinit__(self): self._cparser = \ PyMem_Malloc(sizeof(cparser.http_parser)) if self._cparser is NULL: raise MemoryError() self._csettings = \ PyMem_Malloc(sizeof(cparser.http_parser_settings)) if self._csettings is NULL: raise MemoryError() def __dealloc__(self): PyMem_Free(self._cparser) PyMem_Free(self._csettings) cdef _init(self, protocol, cparser.http_parser_type mode): cparser.http_parser_init(self._cparser, mode) self._cparser.data = self cparser.http_parser_settings_init(self._csettings) self._current_header_name = None self._current_header_value = None self._proto_on_header = getattr(protocol, 'on_header', None) if self._proto_on_header is not None: self._csettings.on_header_field = cb_on_header_field self._csettings.on_header_value = cb_on_header_value self._proto_on_headers_complete = getattr( protocol, 'on_headers_complete', None) self._csettings.on_headers_complete = cb_on_headers_complete self._proto_on_body = getattr(protocol, 'on_body', None) if self._proto_on_body is not None: self._csettings.on_body = cb_on_body self._proto_on_message_begin = getattr( protocol, 'on_message_begin', None) if self._proto_on_message_begin is not None: self._csettings.on_message_begin = cb_on_message_begin self._proto_on_message_complete = getattr( protocol, 'on_message_complete', None) if self._proto_on_message_complete is not None: self._csettings.on_message_complete = cb_on_message_complete self._proto_on_chunk_header = getattr( protocol, 'on_chunk_header', None) self._csettings.on_chunk_header = cb_on_chunk_header self._proto_on_chunk_complete = getattr( protocol, 'on_chunk_complete', None) self._csettings.on_chunk_complete = cb_on_chunk_complete self._last_error = None cdef _maybe_call_on_header(self): if self._current_header_value is not None: current_header_name = self._current_header_name current_header_value = self._current_header_value self._current_header_name = self._current_header_value = None if self._proto_on_header is not None: self._proto_on_header(current_header_name, current_header_value) cdef _on_header_field(self, bytes field): self._maybe_call_on_header() if self._current_header_name is None: self._current_header_name = field else: self._current_header_name += field cdef _on_header_value(self, bytes val): if self._current_header_value is None: self._current_header_value = val else: # This is unlikely, as mostly HTTP headers are one-line self._current_header_value += val cdef _on_headers_complete(self): self._maybe_call_on_header() if self._proto_on_headers_complete is not None: self._proto_on_headers_complete() cdef _on_chunk_header(self): if (self._current_header_value is not None or self._current_header_name is not None): raise HttpParserError('invalid headers state') if self._proto_on_chunk_header is not None: self._proto_on_chunk_header() cdef _on_chunk_complete(self): self._maybe_call_on_header() if self._proto_on_chunk_complete is not None: self._proto_on_chunk_complete() ### Public API ### def get_http_version(self): cdef cparser.http_parser* parser = self._cparser return '{}.{}'.format(parser.http_major, parser.http_minor) def should_keep_alive(self): return bool(cparser.http_should_keep_alive(self._cparser)) def should_upgrade(self): cdef cparser.http_parser* parser = self._cparser return bool(parser.upgrade) def feed_data(self, data): cdef: size_t data_len size_t nb Py_buffer *buf if PyMemoryView_Check(data): buf = PyMemoryView_GET_BUFFER(data) data_len = buf.len nb = cparser.http_parser_execute( self._cparser, self._csettings, buf.buf, data_len) else: buf = &self.py_buf PyObject_GetBuffer(data, buf, PyBUF_SIMPLE) data_len = buf.len nb = cparser.http_parser_execute( self._cparser, self._csettings, buf.buf, data_len) PyBuffer_Release(buf) if self._cparser.http_errno != cparser.HPE_OK: ex = parser_error_from_errno( self._cparser.http_errno) if isinstance(ex, HttpParserCallbackError): if self._last_error is not None: ex.__context__ = self._last_error self._last_error = None raise ex if self._cparser.upgrade: raise HttpParserUpgrade(nb) if nb != data_len: raise HttpParserError('not all of the data was parsed') cdef class HttpRequestParser(HttpParser): def __init__(self, protocol): self._init(protocol, cparser.HTTP_REQUEST) self._proto_on_url = getattr(protocol, 'on_url', None) if self._proto_on_url is not None: self._csettings.on_url = cb_on_url def get_method(self): cdef cparser.http_parser* parser = self._cparser return cparser.http_method_str( parser.method) cdef class HttpResponseParser(HttpParser): def __init__(self, protocol): self._init(protocol, cparser.HTTP_RESPONSE) self._proto_on_status = getattr(protocol, 'on_status', None) if self._proto_on_status is not None: self._csettings.on_status = cb_on_status def get_status_code(self): cdef cparser.http_parser* parser = self._cparser return parser.status_code cdef int cb_on_message_begin(cparser.http_parser* parser) except -1: cdef HttpParser pyparser = parser.data try: pyparser._proto_on_message_begin() except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_url(cparser.http_parser* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data try: pyparser._proto_on_url(at[:length]) except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_status(cparser.http_parser* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data try: pyparser._proto_on_status(at[:length]) except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_header_field(cparser.http_parser* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data try: pyparser._on_header_field(at[:length]) except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_header_value(cparser.http_parser* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data try: pyparser._on_header_value(at[:length]) except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_headers_complete(cparser.http_parser* parser) except -1: cdef HttpParser pyparser = parser.data try: pyparser._on_headers_complete() except BaseException as ex: pyparser._last_error = ex return -1 else: if pyparser._cparser.upgrade: return 1 else: return 0 cdef int cb_on_body(cparser.http_parser* parser, const char *at, size_t length) except -1: cdef HttpParser pyparser = parser.data try: pyparser._proto_on_body(at[:length]) except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_message_complete(cparser.http_parser* parser) except -1: cdef HttpParser pyparser = parser.data try: pyparser._proto_on_message_complete() except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_chunk_header(cparser.http_parser* parser) except -1: cdef HttpParser pyparser = parser.data try: pyparser._on_chunk_header() except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef int cb_on_chunk_complete(cparser.http_parser* parser) except -1: cdef HttpParser pyparser = parser.data try: pyparser._on_chunk_complete() except BaseException as ex: pyparser._last_error = ex return -1 else: return 0 cdef parser_error_from_errno(cparser.http_errno errno): cdef bytes desc = cparser.http_errno_description(errno) if errno in (cparser.HPE_CB_message_begin, cparser.HPE_CB_url, cparser.HPE_CB_header_field, cparser.HPE_CB_header_value, cparser.HPE_CB_headers_complete, cparser.HPE_CB_body, cparser.HPE_CB_message_complete, cparser.HPE_CB_status, cparser.HPE_CB_chunk_header, cparser.HPE_CB_chunk_complete): cls = HttpParserCallbackError elif errno == cparser.HPE_INVALID_STATUS: cls = HttpParserInvalidStatusError elif errno == cparser.HPE_INVALID_METHOD: cls = HttpParserInvalidMethodError elif errno == cparser.HPE_INVALID_URL: cls = HttpParserInvalidURLError else: cls = HttpParserError return cls(desc.decode('latin-1')) @cython.freelist(250) cdef class URL: cdef readonly bytes schema cdef readonly bytes host cdef readonly object port cdef readonly bytes path cdef readonly bytes query cdef readonly bytes fragment cdef readonly bytes userinfo def __cinit__(self, bytes schema, bytes host, object port, bytes path, bytes query, bytes fragment, bytes userinfo): self.schema = schema self.host = host self.port = port self.path = path self.query = query self.fragment = fragment self.userinfo = userinfo def __repr__(self): return ('' .format(self.schema, self.host, self.port, self.path, self.query, self.fragment, self.userinfo)) def parse_url(url): cdef: Py_buffer py_buf char* buf_data cparser.http_parser_url* parsed int res bytes schema = None bytes host = None object port = None bytes path = None bytes query = None bytes fragment = None bytes userinfo = None object result = None int off int ln parsed = \ PyMem_Malloc(sizeof(cparser.http_parser_url)) cparser.http_parser_url_init(parsed) PyObject_GetBuffer(url, &py_buf, PyBUF_SIMPLE) try: buf_data = py_buf.buf res = cparser.http_parser_parse_url(buf_data, py_buf.len, 0, parsed) if res == 0: if parsed.field_set & (1 << cparser.UF_SCHEMA): off = parsed.field_data[cparser.UF_SCHEMA].off ln = parsed.field_data[cparser.UF_SCHEMA].len schema = buf_data[off:off+ln] if parsed.field_set & (1 << cparser.UF_HOST): off = parsed.field_data[cparser.UF_HOST].off ln = parsed.field_data[cparser.UF_HOST].len host = buf_data[off:off+ln] if parsed.field_set & (1 << cparser.UF_PORT): port = parsed.port if parsed.field_set & (1 << cparser.UF_PATH): off = parsed.field_data[cparser.UF_PATH].off ln = parsed.field_data[cparser.UF_PATH].len path = buf_data[off:off+ln] if parsed.field_set & (1 << cparser.UF_QUERY): off = parsed.field_data[cparser.UF_QUERY].off ln = parsed.field_data[cparser.UF_QUERY].len query = buf_data[off:off+ln] if parsed.field_set & (1 << cparser.UF_FRAGMENT): off = parsed.field_data[cparser.UF_FRAGMENT].off ln = parsed.field_data[cparser.UF_FRAGMENT].len fragment = buf_data[off:off+ln] if parsed.field_set & (1 << cparser.UF_USERINFO): off = parsed.field_data[cparser.UF_USERINFO].off ln = parsed.field_data[cparser.UF_USERINFO].len userinfo = buf_data[off:off+ln] return URL(schema, host, port, path, query, fragment, userinfo) else: raise HttpParserInvalidURLError("invalid url {!r}".format(url)) finally: PyBuffer_Release(&py_buf) PyMem_Free(parsed) httptools-0.1.1/httptools/parser/python.pxd000066400000000000000000000002121361736602100211430ustar00rootroot00000000000000cimport cpython cdef extern from "Python.h": cpython.Py_buffer* PyMemoryView_GET_BUFFER(object) bint PyMemoryView_Check(object) httptools-0.1.1/pytest.ini000066400000000000000000000001261361736602100156060ustar00rootroot00000000000000[pytest] addopts = --capture=no --assert=plain --strict --tb native testpaths = tests httptools-0.1.1/setup.py000066400000000000000000000136071361736602100152770ustar00rootroot00000000000000import sys vi = sys.version_info if vi < (3, 5): raise RuntimeError('httptools require Python 3.5 or greater') else: import os.path import pathlib from setuptools import setup, Extension from setuptools.command.build_ext import build_ext as build_ext CFLAGS = ['-O2'] ROOT = pathlib.Path(__file__).parent CYTHON_DEPENDENCY = 'Cython==0.29.14' class httptools_build_ext(build_ext): user_options = build_ext.user_options + [ ('cython-always', None, 'run cythonize() even if .c files are present'), ('cython-annotate', None, 'Produce a colorized HTML version of the Cython source.'), ('cython-directives=', None, 'Cythion compiler directives'), ('use-system-http-parser', None, 'Use the system provided http-parser, instead of the bundled one'), ] boolean_options = build_ext.boolean_options + [ 'cython-always', 'cython-annotate', 'use-system-http-parser', ] def initialize_options(self): # initialize_options() may be called multiple times on the # same command object, so make sure not to override previously # set options. if getattr(self, '_initialized', False): return super().initialize_options() self.use_system_http_parser = False self.cython_always = False self.cython_annotate = None self.cython_directives = None def finalize_options(self): # finalize_options() may be called multiple times on the # same command object, so make sure not to override previously # set options. if getattr(self, '_initialized', False): return need_cythonize = self.cython_always cfiles = {} for extension in self.distribution.ext_modules: for i, sfile in enumerate(extension.sources): if sfile.endswith('.pyx'): prefix, ext = os.path.splitext(sfile) cfile = prefix + '.c' if os.path.exists(cfile) and not self.cython_always: extension.sources[i] = cfile else: if os.path.exists(cfile): cfiles[cfile] = os.path.getmtime(cfile) else: cfiles[cfile] = 0 need_cythonize = True if need_cythonize: try: import Cython except ImportError: raise RuntimeError( 'please install Cython to compile httptools from source') if Cython.__version__ < '0.29': raise RuntimeError( 'httptools requires Cython version 0.29 or greater') from Cython.Build import cythonize directives = {} if self.cython_directives: for directive in self.cython_directives.split(','): k, _, v = directive.partition('=') if v.lower() == 'false': v = False if v.lower() == 'true': v = True directives[k] = v self.distribution.ext_modules[:] = cythonize( self.distribution.ext_modules, compiler_directives=directives, annotate=self.cython_annotate) super().finalize_options() self._initialized = True def build_extensions(self): if self.use_system_http_parser: self.compiler.add_library('http_parser') if sys.platform == 'darwin' and \ os.path.exists('/opt/local/include'): # Support macports on Mac OS X. self.compiler.add_include_dir('/opt/local/include') else: self.compiler.add_include_dir(str(ROOT / 'vendor' / 'http-parser')) self.distribution.ext_modules[0].sources.append( 'vendor/http-parser/http_parser.c') super().build_extensions() with open(str(ROOT / 'README.md')) as f: long_description = f.read() with open(str(ROOT / 'httptools' / '_version.py')) as f: for line in f: if line.startswith('__version__ ='): _, _, version = line.partition('=') VERSION = version.strip(" \n'\"") break else: raise RuntimeError( 'unable to read the version from httptools/_version.py') setup_requires = [] if (not (ROOT / 'httptools' / 'parser' / 'parser.c').exists() or '--cython-always' in sys.argv): # No Cython output, require Cython to build. setup_requires.append(CYTHON_DEPENDENCY) setup( name='httptools', version=VERSION, description='A collection of framework independent HTTP protocol utils.', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/MagicStack/httptools', classifiers=[ 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Programming Language :: Python :: 3', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', 'Environment :: Web Environment', 'Development Status :: 5 - Production/Stable', ], platforms=['macOS', 'POSIX', 'Windows'], zip_safe=False, author='Yury Selivanov', author_email='yury@magic.io', license='MIT', packages=['httptools', 'httptools.parser'], cmdclass={ 'build_ext': httptools_build_ext, }, ext_modules=[ Extension( "httptools.parser.parser", sources=[ "httptools/parser/parser.pyx", ], extra_compile_args=CFLAGS, ), ], include_package_data=True, test_suite='tests.suite', setup_requires=setup_requires, extras_require={ 'test': [ CYTHON_DEPENDENCY ] } ) httptools-0.1.1/tests/000077500000000000000000000000001361736602100147205ustar00rootroot00000000000000httptools-0.1.1/tests/__init__.py000066400000000000000000000006021361736602100170270ustar00rootroot00000000000000import os.path import sys import unittest import unittest.runner def suite(): test_loader = unittest.TestLoader() test_suite = test_loader.discover( os.path.dirname(__file__), pattern='test_*.py') return test_suite if __name__ == '__main__': runner = unittest.runner.TextTestRunner() result = runner.run(suite()) sys.exit(not result.wasSuccessful()) httptools-0.1.1/tests/test_parser.py000066400000000000000000000460661361736602100176410ustar00rootroot00000000000000import httptools import unittest from unittest import mock RESPONSE1_HEAD = b'''HTTP/1.1 200 OK Date: Mon, 23 May 2005 22:38:34 GMT Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux) Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT ETag: "3f80f-1b6-3e1cb03b" Content-Type: text/html; charset=UTF-8 Content-Length: 130 Accept-Ranges: bytes Connection: close ''' RESPONSE1_BODY = b''' An Example Page Hello World, this is a very simple HTML document. ''' CHUNKED_REQUEST1_1 = b'''POST /test.php?a=b+c HTTP/1.2 User-Agent: Fooo Host: bar Transfer-Encoding: chunked 5\r\nhello\r\n6\r\n world\r\n''' CHUNKED_REQUEST1_2 = b'''0\r\nVary: *\r\nUser-Agent: spam\r\n\r\n''' CHUNKED_REQUEST1_3 = b'''POST /test.php?a=b+c HTTP/1.2 User-Agent: Fooo Host: bar Transfer-Encoding: chunked b\r\n+\xce\xcfM\xb5MI,I\x04\x00\r\n0\r\n\r\n''' UPGRADE_REQUEST1 = b'''GET /demo HTTP/1.1 Host: example.com Connection: Upgrade Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 Sec-WebSocket-Protocol: sample Upgrade: WebSocket Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5 Origin: http://example.com Hot diggity dogg''' UPGRADE_RESPONSE1 = b'''HTTP/1.1 101 Switching Protocols UPGRADE: websocket SEC-WEBSOCKET-ACCEPT: rVg+XakFNFOxk3ZH0lzrZBmg0aU= TRANSFER-ENCODING: chunked CONNECTION: upgrade DATE: Sat, 07 May 2016 23:44:32 GMT SERVER: Python/3.4 aiohttp/1.0.3 data'''.replace(b'\n', b'\r\n') class TestResponseParser(unittest.TestCase): def test_parser_response_1(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ p = httptools.HttpResponseParser(m) p.feed_data(memoryview(RESPONSE1_HEAD)) self.assertEqual(p.get_http_version(), '1.1') self.assertEqual(p.get_status_code(), 200) m.on_status.assert_called_once_with(b'OK') m.on_headers_complete.assert_called_once_with() self.assertEqual(m.on_header.call_count, 8) self.assertEqual(len(headers), 8) self.assertEqual(headers.get(b'Connection'), b'close') self.assertEqual(headers.get(b'Content-Type'), b'text/html; charset=UTF-8') self.assertFalse(m.on_body.called) p.feed_data(bytearray(RESPONSE1_BODY)) m.on_body.assert_called_once_with(RESPONSE1_BODY) m.on_message_complete.assert_called_once_with() self.assertFalse(m.on_url.called) self.assertFalse(m.on_chunk_header.called) self.assertFalse(m.on_chunk_complete.called) with self.assertRaisesRegex( httptools.HttpParserError, 'data received after completed connection'): p.feed_data(b'12123123') def test_parser_response_2(self): with self.assertRaisesRegex(TypeError, 'a bytes-like object'): httptools.HttpResponseParser(None).feed_data('') def test_parser_response_3(self): callbacks = {'on_header', 'on_headers_complete', 'on_body', 'on_message_complete'} for cbname in callbacks: with self.subTest('{} callback fails correctly'.format(cbname)): with self.assertRaisesRegex(httptools.HttpParserCallbackError, 'callback failed'): m = mock.Mock() getattr(m, cbname).side_effect = Exception() p = httptools.HttpResponseParser(m) p.feed_data(RESPONSE1_HEAD + RESPONSE1_BODY) def test_parser_response_4(self): p = httptools.HttpResponseParser(None) with self.assertRaises(httptools.HttpParserInvalidStatusError): p.feed_data(b'HTTP/1.1 1299 FOOSPAM\r\n') def test_parser_response_5(self): m = mock.Mock() m.on_status = None m.on_header = None m.on_body = None m.on_headers_complete = None m.on_chunk_header = None m.on_chunk_complete = None p = httptools.HttpResponseParser(m) p.feed_data(RESPONSE1_HEAD) p.feed_data(RESPONSE1_BODY) m.on_message_complete.assert_called_once_with() def test_parser_response_cb_on_status_1(self): class Error(Exception): pass m = mock.Mock() m.on_status.side_effect = Error() p = httptools.HttpResponseParser(m) try: p.feed_data(RESPONSE1_HEAD + RESPONSE1_BODY) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_response_cb_on_body_1(self): class Error(Exception): pass m = mock.Mock() m.on_body.side_effect = Error() p = httptools.HttpResponseParser(m) try: p.feed_data(RESPONSE1_HEAD + RESPONSE1_BODY) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_response_cb_on_message_complete_1(self): class Error(Exception): pass m = mock.Mock() m.on_message_complete.side_effect = Error() p = httptools.HttpResponseParser(m) try: p.feed_data(RESPONSE1_HEAD + RESPONSE1_BODY) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_upgrade_response_1(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ p = httptools.HttpResponseParser(m) try: p.feed_data(UPGRADE_RESPONSE1) except httptools.HttpParserUpgrade as ex: offset = ex.args[0] else: self.fail('HttpParserUpgrade was not raised') self.assertEqual(UPGRADE_RESPONSE1[offset:], b'data') self.assertEqual(p.get_http_version(), '1.1') self.assertEqual(p.get_status_code(), 101) m.on_status.assert_called_once_with(b'Switching Protocols') m.on_headers_complete.assert_called_once_with() self.assertEqual(m.on_header.call_count, 6) self.assertEqual(len(headers), 6) m.on_message_complete.assert_called_once_with() class TestRequestParser(unittest.TestCase): def test_parser_request_chunked_1(self): m = mock.Mock() p = httptools.HttpRequestParser(m) p.feed_data(CHUNKED_REQUEST1_1) self.assertEqual(p.get_method(), b'POST') m.on_message_begin.assert_called_once_with() m.on_url.assert_called_once_with(b'/test.php?a=b+c') self.assertEqual(p.get_http_version(), '1.2') m.on_header.assert_called_with(b'Transfer-Encoding', b'chunked') m.on_chunk_header.assert_called_with() m.on_chunk_complete.assert_called_with() self.assertFalse(m.on_message_complete.called) m.on_message_begin.assert_called_once_with() m.reset_mock() p.feed_data(CHUNKED_REQUEST1_2) m.on_chunk_header.assert_called_with() m.on_chunk_complete.assert_called_with() m.on_header.assert_called_with(b'User-Agent', b'spam') self.assertEqual(m.on_header.call_count, 2) self.assertFalse(m.on_message_begin.called) m.on_message_complete.assert_called_once_with() def test_parser_request_chunked_2(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ m.on_url = None m.on_body = None m.on_headers_complete = None m.on_chunk_header = None m.on_chunk_complete = None p = httptools.HttpRequestParser(m) p.feed_data(CHUNKED_REQUEST1_1) p.feed_data(CHUNKED_REQUEST1_2) self.assertEqual( headers, {b'User-Agent': b'spam', b'Transfer-Encoding': b'chunked', b'Host': b'bar', b'Vary': b'*'}) def test_parser_request_chunked_cb_error_1(self): class Error(Exception): pass m = mock.Mock() m.on_chunk_header.side_effect = Error() p = httptools.HttpRequestParser(m) try: p.feed_data(CHUNKED_REQUEST1_1) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_request_chunked_cb_error_2(self): class Error(Exception): pass m = mock.Mock() m.on_chunk_complete.side_effect = Error() p = httptools.HttpRequestParser(m) try: p.feed_data(CHUNKED_REQUEST1_1) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_request_chunked_3(self): m = mock.Mock() p = httptools.HttpRequestParser(m) p.feed_data(CHUNKED_REQUEST1_3) self.assertEqual(p.get_method(), b'POST') m.on_url.assert_called_once_with(b'/test.php?a=b+c') self.assertEqual(p.get_http_version(), '1.2') m.on_header.assert_called_with(b'Transfer-Encoding', b'chunked') m.on_chunk_header.assert_called_with() m.on_chunk_complete.assert_called_with() self.assertTrue(m.on_message_complete.called) def test_parser_request_upgrade_1(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ p = httptools.HttpRequestParser(m) try: p.feed_data(UPGRADE_REQUEST1) except httptools.HttpParserUpgrade as ex: offset = ex.args[0] else: self.fail('HttpParserUpgrade was not raised') self.assertEqual(UPGRADE_REQUEST1[offset:], b'Hot diggity dogg') self.assertEqual(headers, { b'Sec-WebSocket-Key2': b'12998 5 Y3 1 .P00', b'Sec-WebSocket-Key1': b'4 @1 46546xW%0l 1 5', b'Connection': b'Upgrade', b'Origin': b'http://example.com', b'Sec-WebSocket-Protocol': b'sample', b'Host': b'example.com', b'Upgrade': b'WebSocket'}) def test_parser_request_upgrade_flag(self): class Protocol: def __init__(self): self.parser = httptools.HttpRequestParser(self) def on_url(self, url): assert self.parser.should_upgrade() is False def on_headers_complete(self): assert self.parser.should_upgrade() is True def on_message_complete(self): assert self.parser.should_upgrade() is True protocol = Protocol() try: protocol.parser.feed_data(UPGRADE_REQUEST1) except httptools.HttpParserUpgrade: # Raise as usual. pass else: self.fail('HttpParserUpgrade was not raised') def test_parser_request_error_in_on_header(self): class Error(Exception): pass m = mock.Mock() m.on_header.side_effect = Error() p = httptools.HttpRequestParser(m) try: p.feed_data(UPGRADE_REQUEST1) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_request_error_in_on_message_begin(self): class Error(Exception): pass m = mock.Mock() m.on_message_begin.side_effect = Error() p = httptools.HttpRequestParser(m) try: p.feed_data(UPGRADE_REQUEST1) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_request_error_in_cb_on_url(self): class Error(Exception): pass m = mock.Mock() m.on_url.side_effect = Error() p = httptools.HttpRequestParser(m) try: p.feed_data(UPGRADE_REQUEST1) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_request_error_in_cb_on_headers_complete(self): class Error(Exception): pass m = mock.Mock() m.on_headers_complete.side_effect = Error() p = httptools.HttpRequestParser(m) try: p.feed_data(UPGRADE_REQUEST1) except httptools.HttpParserCallbackError as ex: self.assertIsInstance(ex.__context__, Error) else: self.fail('HttpParserCallbackError was not raised') def test_parser_request_2(self): p = httptools.HttpRequestParser(None) with self.assertRaises(httptools.HttpParserInvalidMethodError): p.feed_data(b'SPAM /test.php?a=b+c HTTP/1.2') def test_parser_request_3(self): p = httptools.HttpRequestParser(None) with self.assertRaises(httptools.HttpParserInvalidURLError): p.feed_data(b'POST HTTP/1.2') def test_parser_request_4(self): p = httptools.HttpRequestParser(None) with self.assertRaisesRegex(TypeError, 'a bytes-like object'): p.feed_data('POST HTTP/1.2') def test_parser_request_fragmented(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ p = httptools.HttpRequestParser(m) REQUEST = ( b'PUT / HTTP/1.1\r\nHost: localhost:1234\r\nContent-Type: text/pl', b'ain; charset=utf-8\r\nX-Empty-Header: \r\nConnection: close\r\n', b'Content-Length: 10\r\n\r\n1234567890', ) p.feed_data(REQUEST[0]) m.on_message_begin.assert_called_once_with() m.on_url.assert_called_once_with(b'/') self.assertEqual(headers, {b'Host': b'localhost:1234'}) p.feed_data(REQUEST[1]) self.assertEqual( headers, {b'Host': b'localhost:1234', b'Content-Type': b'text/plain; charset=utf-8', b'X-Empty-Header': b''}) p.feed_data(REQUEST[2]) self.assertEqual( headers, {b'Host': b'localhost:1234', b'Content-Type': b'text/plain; charset=utf-8', b'X-Empty-Header': b'', b'Connection': b'close', b'Content-Length': b'10'}) m.on_message_complete.assert_called_once_with() def test_parser_request_fragmented_header(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ p = httptools.HttpRequestParser(m) REQUEST = ( b'PUT / HTTP/1.1\r\nHost: localhost:1234\r\nContent-', b'Type: text/plain; charset=utf-8\r\n\r\n', ) p.feed_data(REQUEST[0]) m.on_message_begin.assert_called_once_with() m.on_url.assert_called_once_with(b'/') self.assertEqual(headers, {b'Host': b'localhost:1234'}) p.feed_data(REQUEST[1]) self.assertEqual( headers, {b'Host': b'localhost:1234', b'Content-Type': b'text/plain; charset=utf-8'}) def test_parser_request_fragmented_value(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ p = httptools.HttpRequestParser(m) REQUEST = ( b'PUT / HTTP/1.1\r\nHost: localhost:1234\r\nContent-Type:', b' text/pla', b'in; chars', b'et=utf-8\r\n\r\n', ) p.feed_data(REQUEST[0]) m.on_message_begin.assert_called_once_with() m.on_url.assert_called_once_with(b'/') self.assertEqual(headers, {b'Host': b'localhost:1234'}) p.feed_data(REQUEST[1]) p.feed_data(REQUEST[2]) p.feed_data(REQUEST[3]) self.assertEqual( headers, {b'Host': b'localhost:1234', b'Content-Type': b'text/plain; charset=utf-8'}) def test_parser_request_fragmented_bytes(self): m = mock.Mock() headers = {} m.on_header.side_effect = headers.__setitem__ p = httptools.HttpRequestParser(m) REQUEST = \ b'PUT / HTTP/1.1\r\nHost: localhost:1234\r\nContent-' \ b'Type: text/plain; charset=utf-8\r\n\r\n' step = 1 for i in range(0, len(REQUEST), step): p.feed_data(REQUEST[i:i+step]) self.assertEqual( headers, {b'Host': b'localhost:1234', b'Content-Type': b'text/plain; charset=utf-8'}) class TestUrlParser(unittest.TestCase): def parse(self, url:bytes): parsed = httptools.parse_url(url) return (parsed.schema, parsed.host, parsed.port, parsed.path, parsed.query, parsed.fragment, parsed.userinfo) def test_parser_url_1(self): self.assertEqual( self.parse(b'dsf://aaa/b/c?aa#123'), (b'dsf', b'aaa', None, b'/b/c', b'aa', b'123', None)) self.assertEqual( self.parse(b'dsf://i:n@aaa:88/b/c?aa#123'), (b'dsf', b'aaa', 88, b'/b/c', b'aa', b'123', b'i:n')) self.assertEqual( self.parse(b'////'), (None, None, None, b'////', None, None, None)) self.assertEqual( self.parse(b'////1/1?a=b&c[]=d&c[]=z'), (None, None, None, b'////1/1', b'a=b&c[]=d&c[]=z', None, None)) self.assertEqual( self.parse(b'/////?#123'), (None, None, None, b'/////', None, b'123', None)) self.assertEqual( self.parse(b'/a/b/c?b=1&'), (None, None, None, b'/a/b/c', b'b=1&', None, None)) def test_parser_url_2(self): with self.assertRaises(httptools.HttpParserInvalidURLError): self.parse(b'') def test_parser_url_3(self): with self.assertRaises(httptools.HttpParserInvalidURLError): self.parse(b' ') def test_parser_url_4(self): with self.assertRaises(httptools.HttpParserInvalidURLError): self.parse(b':///1') def test_parser_url_5(self): self.assertEqual( self.parse(b'http://[1:2::3:4]:67/'), (b'http', b'1:2::3:4', 67, b'/', None, None, None)) def test_parser_url_6(self): self.assertEqual( self.parse(bytearray(b'/')), (None, None, None, b'/', None, None, None)) def test_parser_url_7(self): url = httptools.parse_url(b'/') with self.assertRaisesRegex(AttributeError, 'not writable'): url.port = 0 def test_parser_url_8(self): with self.assertRaises(TypeError): httptools.parse_url(None) def test_parser_url_9(self): with self.assertRaisesRegex(httptools.HttpParserInvalidURLError, r'a\\x00aa'): self.parse(b'dsf://a\x00aa') def test_parser_url_10(self): with self.assertRaisesRegex(TypeError, 'a bytes-like object'): self.parse('dsf://aaa') httptools-0.1.1/vendor/000077500000000000000000000000001361736602100150535ustar00rootroot00000000000000httptools-0.1.1/vendor/http-parser/000077500000000000000000000000001361736602100173245ustar00rootroot00000000000000