diff --git a/README.md b/README.md index 7b307b3d..7994466b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/plugin_examples.py b/plugin_examples.py index e970bb7d..8f4aec4e 100644 --- a/plugin_examples.py +++ b/plugin_examples.py @@ -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 diff --git a/proxy.py b/proxy.py index df8c696f..7229b140 100755 --- a/proxy.py +++ b/proxy.py @@ -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: