Add ProposedRestApiPlugin example. (#101)

* Add ProposedRestApiPlugin example.

This plugin is an example to demonstrate how `proxy.py` can help you
test and debug client applications without actually having a need of
backend server.

Simply modify ProposedRestApiPlugin as per need to return json
responses for your API specs.

* Add README instructions

* Correct username
This commit is contained in:
Abhinav Singh 2019-09-27 13:00:11 -07:00 committed by GitHub
parent 3a9c8bc8a2
commit c302537572
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 113 additions and 15 deletions

View File

@ -36,6 +36,7 @@ Table of Contents
* [Command Line](#command-line)
* [Docker Image](#docker-image)
* [Plugin Examples](#plugin-examples)
* [ProposedRestApiPlugin](#proposedrestapiplugin)
* [RedirectToCustomServerPlugin](#redirecttocustomserverplugin)
* [FilterByUpstreamHostPlugin](#filterbyupstreamhostplugin)
* [CacheResponsesPlugin](#cacheresponsesplugin)
@ -167,6 +168,37 @@ See [plugin_examples.py](https://github.com/abhinavsingh/proxy.py/blob/develop/p
All the examples below also works with `https` traffic but require additional flags and certificate generation.
See [TLS Interception](#tls-interception).
## ProposedRestApiPlugin
Mock responses for your server REST API.
Use to test and develop client side applications
without need of an actual upstream REST API server.
Start `proxy.py` as:
```
$ proxy.py \
--plugins plugin_examples.ProposedRestApiPlugin
```
Verify mock API response using `curl -x localhost:8899 http://api.example.com/v1/users/`
```
{"count": 2, "next": null, "previous": null, "results": [{"email": "you@example.com", "groups": [], "url": "api.example.com/v1/users/1/", "username": "admin"}, {"email": "someone@example.com", "groups": [], "url": "api.example.com/v1/users/2/", "username": "admin"}]}
```
Verify the same by inspecting `proxy.py` logs:
```
2019-09-27 12:44:02,212 - INFO - pid:7077 - access_log:1210 - ::1:64792 - GET None:None/v1/users/ - None None - 0 byte
```
Access log shows `None:None` as server `ip:port`. `None` simply means that
the server connection was never made, since response was returned by our plugin.
Now modify `ProposedRestApiPlugin` to returns REST API mock
responses as expected by your clients.
## RedirectToCustomServerPlugin
Redirects all incoming `http` requests to custom web server.

View File

@ -6,6 +6,7 @@
:copyright: (c) 2013-present by Abhinav Singh.
:license: BSD, see LICENSE for more details.
"""
import json
import os
import tempfile
import time
@ -17,19 +18,80 @@ import proxy
@proxy.route(b'/hello-world')
def hello_world(_request: proxy.HttpParser) -> bytes:
"""A HttpWebServerRoutePlugin plugin for inbuilt web server."""
return proxy.HttpParser.build_response(200, body=b'Hello World')
class ProposedRestApiPlugin(proxy.HttpProxyBasePlugin):
"""Mock responses for your upstream REST API.
Used to test and develop client side applications
without need of an actual upstream REST API server.
Returns proposed REST API mock responses to the client."""
API_SERVER = b'api.example.com'
REST_API_SPEC = {
b'/v1/users/': {
'count': 2,
'next': None,
'previous': None,
'results': [
{
'email': 'you@example.com',
'groups': [],
'url': proxy.text_(API_SERVER) + '/v1/users/1/',
'username': 'admin',
},
{
'email': 'someone@example.com',
'groups': [],
'url': proxy.text_(API_SERVER) + '/v1/users/2/',
'username': 'someone',
},
]
},
}
def before_upstream_connection(self) -> bool:
"""Called after client request is received and
before connecting to upstream server."""
if self.request.host == self.API_SERVER and self.request.url:
if self.request.url.path in self.REST_API_SPEC:
self.client.send(proxy.HttpParser.build_response(
200, reason=b'OK',
headers={b'Content-Type': b'application/json'},
body=proxy.bytes_(json.dumps(self.REST_API_SPEC[self.request.url.path]))
))
else:
self.client.send(proxy.HttpParser.build_response(
404, reason=b'NOT FOUND', body=b'Not Found'
))
return True
return False
def on_upstream_connection(self) -> None:
pass
def handle_upstream_response(self, raw: bytes) -> bytes:
return raw
def on_upstream_connection_close(self) -> None:
pass
class RedirectToCustomServerPlugin(proxy.HttpProxyBasePlugin):
"""Modifies client request to redirect all incoming requests to a fixed server address."""
UPSTREAM_SERVER = b'http://localhost:8899'
def before_upstream_connection(self) -> None:
def before_upstream_connection(self) -> bool:
# Redirect all non-https requests to inbuilt WebServer.
if self.request.method != b'CONNECT':
self.request.url = urlparse.urlsplit(self.UPSTREAM_SERVER)
self.request.set_host_port()
return False
def on_upstream_connection(self) -> None:
pass
@ -46,10 +108,11 @@ class FilterByUpstreamHostPlugin(proxy.HttpProxyBasePlugin):
FILTERED_DOMAINS = [b'google.com', b'www.google.com']
def before_upstream_connection(self) -> None:
def before_upstream_connection(self) -> bool:
if self.request.host in self.FILTERED_DOMAINS:
raise proxy.HttpRequestRejected(
status_code=418, reason=b'I\'m a tea pot')
return False
def on_upstream_connection(self) -> None:
pass
@ -72,8 +135,8 @@ class CacheResponsesPlugin(proxy.HttpProxyBasePlugin):
self.cache_file_path: Optional[str] = None
self.cache_file: Optional[BinaryIO] = None
def before_upstream_connection(self) -> None:
pass
def before_upstream_connection(self) -> bool:
return False
def on_upstream_connection(self) -> None:
self.cache_file_path = os.path.join(
@ -95,8 +158,8 @@ class CacheResponsesPlugin(proxy.HttpProxyBasePlugin):
class ManInTheMiddlePlugin(proxy.HttpProxyBasePlugin):
"""Modifies upstream server responses."""
def before_upstream_connection(self) -> None:
pass
def before_upstream_connection(self) -> bool:
return False
def on_upstream_connection(self) -> None:
pass

View File

@ -958,7 +958,7 @@ class HttpProxyBasePlugin(ABC):
return self.__class__.__name__
@abstractmethod
def before_upstream_connection(self) -> None:
def before_upstream_connection(self) -> bool:
"""Handler called just before Proxy upstream connection is established.
Raise HttpRequestRejected to drop the connection."""
@ -1117,7 +1117,9 @@ class HttpProxyPlugin(ProtocolHandlerPlugin):
# Note: can raise HttpRequestRejected exception
for plugin in self.plugins.values():
plugin.before_upstream_connection()
teardown = plugin.before_upstream_connection()
if teardown:
return teardown
self.authenticate()
self.connect_upstream()
@ -1186,7 +1188,7 @@ class HttpProxyPlugin(ProtocolHandlerPlugin):
if not self.request.has_upstream_server():
return
host, port = self.server.addr if self.server else (None, None)
server_host, server_port = self.server.addr if self.server else (None, None)
if self.request.method == b'CONNECT':
logger.info(
'%s:%s - %s %s:%s - %s bytes' %
@ -1194,17 +1196,18 @@ class HttpProxyPlugin(ProtocolHandlerPlugin):
self.client.addr[1],
text_(
self.request.method),
text_(host),
text_(port),
text_(server_host),
text_(server_port),
self.response.total_size))
elif self.request.method:
logger.info(
'%s:%s - %s %s:%s%s - %s %s - %s bytes' %
(self.client.addr[0], self.client.addr[1], text_(
self.request.method), text_(host), port, text_(
self.request.build_url()), text_(
self.response.code), text_(
self.response.reason), self.response.total_size))
self.request.method), text_(server_host), server_port,
text_(self.request.build_url()),
text_(self.response.code),
text_(self.response.reason),
self.response.total_size))
def authenticate(self) -> None:
if self.config.auth_code: