Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 102 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,104 @@
*.egg-info
/dist/
/build/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.tox
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# dotenv
.env

# virtualenv
.venv
venv/
ENV/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

# visual studio code
.vscode
50 changes: 50 additions & 0 deletions tests/test_curl.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from copy import deepcopy
import json
import socket
import tempfile
import unittest

from uwsgi_tools.curl import cli
from tests.utils import server


class CurlTests(unittest.TestCase):
def setUp(self):
self.datafile = tempfile.NamedTemporaryFile(delete=True)
with open(self.datafile.name, 'w') as fh:
json.dump({'foo': 'bar'}, fh)

def test_cli(self):
with server():
self.assertFalse(cli('127.0.0.1', 'host.name/'))
Expand All @@ -22,3 +31,44 @@ def test_file_socket(self):
def test_headers(self):
with server(callback=lambda x: self.assertIn(b'localhost', x)):
self.assertFalse(cli('127.0.0.1:3030', '-H', 'Host: localhost'))

def test_post(self):
requests = []

def request_sniper(x):
requests.append(deepcopy(x))

with server(callback=request_sniper):
self.assertFalse(cli(
'--method', 'POST',
'--header', 'Content-Type: application/json',
r'''--data=\'{"foo":"bar"}\'''',
'127.0.0.1',
'host.name/'))

self.assertIn(b'POST', requests[0])
# Magic number is the val_size + val
self.assertIn(b'CONTENT_LENGTH\x02\x0017', requests[0])
self.assertIn(b'CONTENT_TYPE', requests[0])
self.assertIn(b'application/json', requests[0])

def test_at_prefixed_data(self):
requests = []

def request_sniper(x):
requests.append(deepcopy(x))

with server(callback=request_sniper):
self.assertFalse(cli(
'--method', 'POST',
'--header', 'Content-Type: application/json',
'--data', '@{}'.format(self.datafile.name),
'127.0.0.1',
'host.name/',
))

self.assertIn(b'POST', requests[0])
# Magic number is the val_size + val
self.assertIn(b'CONTENT_LENGTH\x02\x0014', requests[0])
self.assertIn(b'CONTENT_TYPE', requests[0])
self.assertIn(b'application/json', requests[0])
55 changes: 55 additions & 0 deletions tests/test_uwsgi_structs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import unittest

from uwsgi_tools import uwsgi_structs as uw

"""
From definitions available at
http://uwsgi-docs.readthedocs.io/en/latest/Protocol.html
"""


class TestUwsgiPacketHeader(unittest.TestCase):
header = uw.UwsgiPacketHeader(0, 166, 0)
header_bytes = b'\x00\xa6\x00\x00'

def test_packet_header_serializes(self):
expected = self.header_bytes

self.assertEqual(expected, bytearray(self.header))

def test_packet_header_deserializes(self):
buffer = bytearray(self.header_bytes)
expected_header = uw.UwsgiPacketHeader(0, 166, 0)

header = uw.UwsgiPacketHeader.from_buffer(buffer)
self.assertEqual(
bytearray(expected_header),
bytearray(header)
)


class TestUwsgiVar(unittest.TestCase):

key = b'foo'
key_size = len(key)
val = b'bar'
val_size = len(val)

var = uw.UwsgiVar(key_size, key, val_size, val)
var_bytes = b'\x03\x00foo\x03\x00bar'

def test_var_serializes(self):
self.assertEqual(
self.var_bytes,
bytearray(self.var)
)

def test_var_deserializes(self):
buffer = bytearray(self.var_bytes)
expected = self.var

var = uw.UwsgiVar.from_buffer(buffer)
self.assertEqual(
bytearray(expected),
bytearray(var)
)
8 changes: 4 additions & 4 deletions uwsgi_tools/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

__all__ = [
'BaseHTTPRequestHandler', 'TCPServer', 'get_content_type', 'urlsplit',
'hex2bytes',
'struct2bytes',
]

PY3 = sys.version_info[0] == 3
Expand All @@ -25,8 +25,8 @@ def get_content_type(headers):
return headers.typeheader


def hex2bytes(s):
def struct2bytes(s):
if PY3:
return bytes.fromhex(s)
return bytes(s)
else:
return s.decode('hex')
return bytes(bytearray(s))
23 changes: 17 additions & 6 deletions uwsgi_tools/curl.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@ def ask_uwsgi(uwsgi_addr, var, body='', timeout=0, udp=False):
if timeout:
s.settimeout(timeout)

if body is None:
body = ''

s.connect(addr)
s.send(pack_uwsgi_vars(var) + body.encode('utf8'))
s.send(pack_uwsgi_vars(var) + body)
response = []
while 1:
data = s.recv(4096)
Expand All @@ -45,17 +42,31 @@ def curl(uwsgi_addr, url, method='GET', body='', timeout=0, headers=(),
else:
port = None

body = (body or '').encode('utf-8')

var = {
'SERVER_PROTOCOL': 'HTTP/1.1',
'PATH_INFO': parts_uri.path,
'REQUEST_METHOD': method.upper(),
'REQUEST_URI': uri,
'QUERY_STRING': parts_uri.query,
'HTTP_HOST': host,
'CONTENT_LENGTH': str(len(body)),
# Other varaibles seen in nginx's uwsgi_params file but not explicitly
# handled anywhere in this file
# https://github.com/nginx/nginx/blob/master/conf/uwsgi_params
# DOCUMENT_ROOT
# REQUEST_SCHEME
# HTTPS
# REMOTE_ADDR
# REMOTE_PORT
}

for header in headers or ():
key, _, value = header.partition(':')
var['HTTP_' + key.strip().upper()] = value.strip()
var['HTTP_' + key.strip().upper().replace('-', '_')] = value.strip()
if 'HTTP_CONTENT_TYPE' in var.keys():
var['CONTENT_TYPE'] = var['HTTP_CONTENT_TYPE']
var['SERVER_NAME'] = var['HTTP_HOST']
if port:
var['SERVER_PORT'] = str(port)
Expand All @@ -67,7 +78,7 @@ def curl(uwsgi_addr, url, method='GET', body='', timeout=0, headers=(),
def cli(*args):
import argparse

parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(fromfile_prefix_chars='@')

parser.add_argument('uwsgi_addr', nargs=1,
help="Remote address of uWSGI server")
Expand Down
2 changes: 1 addition & 1 deletion uwsgi_tools/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def do(self):
env.pop('HTTP_REFERER', 0)

cl = env['CONTENT_LENGTH']
body = repr(self.rfile.read(int(cl))) if cl else ''
body = (repr(self.rfile.read(int(cl))) if cl else '').encode('utf-8')

resp = ask_uwsgi((self.server.uwsgi_addr, self.server.uwsgi_port),
var=env, body=body)
Expand Down
23 changes: 12 additions & 11 deletions uwsgi_tools/utils.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from .compat import hex2bytes, urlsplit


def sz(x):
s = hex(x if isinstance(x, int) else len(x))[2:].rjust(4, '0')
s = hex2bytes(s)
return s[::-1]
from .compat import struct2bytes, urlsplit
from .uwsgi_structs import UwsgiPacketHeader, UwsgiVar


def pack_uwsgi_vars(var):
pk = b''
for k, v in var.items() if hasattr(var, 'items') else var:
pk += sz(k) + k.encode('utf8') + sz(v) + v.encode('utf8')
return b'\x00' + sz(pk) + b'\x00' + pk
encoded_vars = [
(k.encode('utf-8'), v.encode('utf-8'))
for k, v in var.items()
]
packed_vars = b''.join(
struct2bytes(UwsgiVar(len(k), k, len(v), v))
for k, v in encoded_vars
)
packet_header = struct2bytes(UwsgiPacketHeader(0, len(packed_vars), 0))
return packet_header + packed_vars


def parse_addr(addr, default_port=3030):
Expand Down
Loading